北京建设网站专家,遵义网站开发制作公司,福田庆三,广州新建站synchronized作为Java程序员最常用同步工具#xff0c;很多人却对它的用法和实现原理一知半解#xff0c;以至于还有不少人认为synchronized是重量级锁#xff0c;性能较差#xff0c;尽量少用。
但不可否认的是synchronized依然是并发首选工具#xff0c;连volatile、CA…synchronized作为Java程序员最常用同步工具很多人却对它的用法和实现原理一知半解以至于还有不少人认为synchronized是重量级锁性能较差尽量少用。
但不可否认的是synchronized依然是并发首选工具连volatile、CAS、ReentrantLock都无法动摇synchronized的地位。synchronized是工作面试中的必备技能今天就跟着一灯一块深入剖析synchronized底层到底做了哪些优化
synchronized是用来加锁的而锁是加在对象上面所以需要先聊一下JVM中对象构成。
1. 对象的构成
Java对象在JVM内存中由三块区域组成对象头、实例数据、对齐填充。
对象头又分为Mark Word标记字段、Class Pointer类型指针、数组长度如果是数组。
实例数据是对象实际有效信息包括本类信息和父类信息等。
对齐填充没有特殊含义由于虚拟机要求 对象起始地址必须是8字节的整数倍作用仅是字节对齐。
Class Pointer是对象指向它的类元数据的指针虚拟机通过这个指针来确定这个对象是哪个类的实例。
重点关注一下对象头中Mark Word里面存储了对象的hashcode、锁状态标识、持有锁的线程id、GC分代年龄等。
在32为的虚拟机中Mark Word的组成如下
2. synchronized锁优化
从JDK1.6开始就对synchronized的实现机制进行了较大调整包括使用JDK1.5引进的CAS自旋之外还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁等优化策略。由于使得synchronized性能极大提高同时语义清晰、操作简单、无需手动关闭所以推荐在允许的情况下尽量使用此关键字同时在性能上此关键字还有优化的空间。
锁主要存在四种状态依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态性能依次是从高到低。锁可以从偏向锁升级到轻量级锁再升级的重量级锁。但是锁的升级是单向的也就是说只能从低到高升级不会出现锁的降级。 在 JDK 1.6 中默认是开启偏向锁和轻量级锁的可以通过-XX:-UseBiasedLocking来禁用偏向锁。 2.1 自旋锁
线程的挂起与恢复需要CPU从用户态转为内核态频繁的阻塞和唤醒对CPU来说是一件负担很重的工作势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面对象锁的锁状态只会持续很短一段时间为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。 自旋锁就是指当一个线程尝试获取某个锁时如果该锁已被其他线程占用就一直循环检测锁是否被释放而不是进入线程挂起或睡眠状态。 自旋锁适用于锁保护的临界区很小的情况临界区很小的话锁占用的时间就很短。自旋等待不能替代阻塞虽然它可以避免线程切换带来的开销但是它占用了CPU处理器的时间。如果持有锁的线程很快就释放了锁那么自旋的效率就非常好反之自旋的线程就会白白消耗掉处理的资源它不会做任何有意义的工作这样反而会带来性能上的浪费。所以说自旋等待的时间自旋的次数必须要有一个限度如果自旋超过了定义的时间仍然没有获取到锁则应该被挂起。 自旋锁在JDK 1.4.2中引入默认关闭但是可以使用-XX:UseSpinning开开启在JDK1.6中默认开启。同时自旋的默认次数为10次可以通过参数-XX:PreBlockSpin来调整 2.2 自适应自旋锁
JDK 1.6引入了更加智能的自旋锁即自适应自旋锁。自适应就意味着自旋的次数不再是固定的它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。那它如何进行适应性自旋呢 线程如果自旋成功了那么下次自旋的次数会更加多因为虚拟机认为既然上次成功了那么此次自旋也很有可能会再次成功那么它就会允许自旋等待持续的次数更多。 反之如果对于某个锁很少有自旋能够成功那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程以免浪费CPU资源。 有了自适应自旋锁随着程序运行和性能监控信息的不断完善虚拟机对程序锁的状况预测会越来越准确虚拟机会变得越来越聪明
2.3 锁消除
JVM在JIT编译时通过对运行上下文的扫描经过逃逸分析对于某段代码不存在竞争或共享的可能性就会讲这段代码的锁消除提升程序运行效率
public void method() {final Object LOCK new Object();synchronized (LOCK) {// do something}
}比如上面代码中锁是方法中私有的又是不可变的完全没必要加锁所以JVM就会执行**锁消除 **
2.4 锁粗化
按理来说同步块的作用范围应该尽可能小仅在共享数据的实际作用域中才进行同步这样做的目的是为了使需要同步的操作数量尽可能缩小缩短阻塞时间如果存在锁竞争那么等待锁的线程也能尽快拿到锁。 但是加锁解锁也需要消耗资源如果存在一系列的连续加锁解锁操作可能会导致不必要的性能损耗 锁粗化就是将多个连续的加锁、解锁操作连接在一起扩展成一个范围更大的锁避免频繁的加锁解锁操作 public void method(Object LOCK) {synchronized (LOCK) {// do something1}synchronized (LOCK) {// do something2}
}比如上面方法中两个加锁的代码块完全可以合并成一个减少频繁加锁解锁带来的开销提升程序运行效率
2.5 偏向锁
为什么要引入偏向锁 因为经过HotSpot的作者大量的研究发现大多数时候是不存在锁竞争的通常是一个线程多次获得同一把锁因此如果每次都要竞争锁会增大很多没有必要付出的代价为了降低获取锁的代价才引入的偏向锁
2.6 轻量级锁
轻量级锁考虑的是竞争锁对象的线程不多而且线程持有锁的时间也不长的场景。因为阻塞线程需要CPU从用户态转到内核态代价较大如果刚刚阻塞不久这个锁就被释放了那这个代价就有点得不偿失了因此这个时候就干脆不阻塞这个线程让它**自旋CAS**这等待锁释放 加锁过程当代码进入同步块时如果同步对象为无锁状态时当前线程会在栈帧中创建一个锁记录(Lock Record)区域同时将锁对象的对象头中 Mark Word 拷贝到锁记录中再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。如果更新成功当前线程就获得了锁。 解锁过程轻量锁的解锁过程也是利用 CAS 来实现的会尝试锁记录替换回锁对象的 Mark Word 。如果替换成功则说明整个同步操作完成失败则说明有其他线程尝试获取锁这时就会唤醒被挂起的线程(此时已经膨胀为重量锁) 2.7 重量级锁
synchronized是通过对象内部的监视器锁Monitor来实现的。但是监视器锁本质又是依赖于底层的操作系统的互斥锁Mutex Lock来实现的。
重量级锁的工作流程当系统检查到锁是重量级锁之后会把等待想要获得锁的线程进行阻塞被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时都需要操作系统来帮忙这就需要从用户态转换到内核态而转换状态是需要消耗很多时间的有可能比用户执行代码的时间还要长所以重量级锁的开销还是很大的。
在锁竞争激烈、锁持有时间长的场景还是适合使用重量级锁的
2.8 锁升级过程 2.9 锁的优缺点对比
锁的性能从低到高依次是无锁、偏向锁、轻量级锁、重量级锁。不同的锁只是适合不同的场景大家可以依据实际场景自行选择
3. 总结
synchronized锁经过多次迭代优化已经不像以前那么重了在JDK1.8的ConcurrentHashMap源码中已经大量使用synchronized做同步控制大家在日常开发中可以放心使用了