JDK6中对synchronized的优化
一、引言
在jdk1.6以前,synchronized只有一种锁,那就是重量级锁,而在jdk1.6的时候,对synchronized做出了巨大的优化,引入了3种其他的锁状态——无锁、偏向锁和轻量级锁,主要使用了锁膨胀、锁消除、锁粗化和自适应自旋锁的优化方法。
二、锁膨胀
-
简介
锁膨胀是指synchronized从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它也叫做锁升级。
-
执行流程
- 无锁状态:初始时,对象处于无锁状态,任何线程都可以访问该对象。
- 偏向锁阶段:当有一个线程访问对象时,对象会尝试偏向于这个线程,这时会在对象头部标记该线程ID,并将锁状态置为偏向锁。这个过程是为了避免多个线程竞争同一个对象的锁,提高性能。在这个阶段,其他线程需要获取锁时,会先尝试撤销偏向锁,然后进入轻量级锁阶段。
- 轻量级锁阶段:当有多个线程竞争对象的锁时,对象会进入轻量级锁阶段。在这个阶段,首先会尝试使用CAS(Compare and Swap)操作来尝试获取锁,如果成功则获得锁,如果失败则会进行一定次数的自旋尝试获取锁。自旋的目的是为了避免线程频繁地进入阻塞状态,提高性能。
- 重量级锁阶段:如果自旋锁尝试获取锁仍然失败,对象会进入重量级锁阶段,这时候线程会进入阻塞状态,等待锁的释放。
-
锁膨胀为什么能优化
锁膨胀(锁升级)能够优化的原因是,它可以根据对象的竞争情况,动态地选择合适的锁机制,从而减少不必要的锁竞争,提高程序的性能。锁膨胀通过动态地选择合适的锁机制,可以在保证程序正确性的前提下,尽可能地减少锁的开销,提高程序的性能。比如,在竞争不激烈的情况下,可以使用偏向锁来避免多个线程竞争同一个对象的锁,从而减少锁的开销;在竞争激烈的情况下,可以使用自旋锁来避免线程频繁地进入阻塞状态,从而减少上下文切换的开销。
三、锁消除
-
简介
锁消除是指在编译器优化阶段,通过静态分析和动态分析,识别出某些情况下不需要进行同步的代码块,从而消除这些同步操作,减少不必要的同步开销,提高程序的性能。
-
执行流程
- 静态分析:编译器在进行静态分析时,会对代码进行分析,识别出一些情况下不可能存在共享资源竞争的情况。例如,编译器可以分析局部变量的作用域,确定某个对象只在方法内部使用,并且不会被传递到其他方法中,或者分析循环体内的对象是否会逃逸出循环等。
- 动态分析:在运行时,JIT 编译器会对代码进行动态分析,观察实际的程序执行情况,确定是否存在共享资源竞争。如果在运行时发现某个锁在实际执行中并没有被多个线程竞争,那么编译器可以进行锁消除优化。
- 锁消除:一旦编译器确定某个同步代码块不需要进行同步操作,就会进行锁消除优化,将同步操作从代码中移除。这样可以减少不必要的同步开销,提高程序的性能。
-
代码示例
public class SynchronizedTest { public void doSomething() { // 对象obj只在方法内部使用,并且不会被传递到其他方法中 Object obj = new Object(); synchronized (obj) { // 这里的同步操作可能会被编译器消除 // 由于obj只在当前方法内部使用,且不会被传递到其他方法中,不存在共享资源竞争的可能 // 因此编译器可以消除这里的同步操作 // ... } // 循环内的对象也可能会被消除 for (int i = 0; i < 10; i++) { Object loopObj = new Object(); synchronized (loopObj) { // 这里的同步操作可能会被编译器消除 // 由于loopObj只在当前循环内部使用,且不会被传递到循环外部,不存在共享资源竞争的可能 // 因此编译器可以消除这里的同步操作 // ... } } } }
四、锁粗化
-
简介
锁粗化是一种针对连续的加锁和解锁操作的优化,它将多个连续的锁操作合并为一个更大的锁范围,以减少锁的竞争和开销。这种优化通常发生在循环内部或者连续的方法调用中。当编译器检测到这种情况时,它会将多个锁操作合并为一个更大的锁范围,从而减少锁的竞争和额外开销。
-
代码示例(优化前)
public class SynchronizedTest { public void doSomething() { Object lock = new Object(); // 连续的加锁和解锁操作 synchronized (lock) { // do something } // 另一个同步块,可能会被合并为一个更大的锁范围 synchronized (lock) { // do something else } } }
-
优化后
public class SynchronizedTest { public void doSomething() { Object lock = new Object(); // 连续的加锁和解锁操作优化后更大范围的synchronized synchronized (lock) { // do something // do something else } } }
五、自适应自旋锁
-
简介
自旋锁是指通过自身循环,尝试获取锁的一种方式。如果长时间自旋还获取不到锁,那么也会造成一定的资源浪费,所以我们通常会给自旋设置一个固定的值来避免一直自旋的性能开销。而synchronized中的自旋锁是自适应的自旋锁,线程自旋的次数不再是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。
-
自适应自旋锁原理
在自旋锁的基础上,自旋尝试的次数和阈值会根据实际运行情况动态调整。比如说它会收集运行时的锁竞争情况和线程调度信息,以动态调整自旋尝试次数和阈值。用大白话说就是,上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。
六、总结
本文讲解了从jdk6开始使用的 4 种优化 synchronized 的方案,其中锁膨胀和自适应自适应自旋锁是 synchronized 关键字自身的优化实现,而锁消除和锁粗化是 JVM 虚拟机对 synchronized 提供的优化方案(编译器级别的),这些优化方案最终使得 synchronized 的性能得到了大幅的提升,也让它在并发编程中占据了一席之地。