Java 多线程学习笔记 07-JVM 对锁的优化

这⾥的锁优化主要是指 JVM 对 synchronized 的优化。

⾃旋锁

互斥同步进⼊阻塞状态的开销都很⼤,应该尽量避免。在许多应⽤中,共享数据的锁定状态只会持续很短的⼀段时间。⾃旋锁的思想是让⼀个线程在请求⼀个共享数据的锁时执⾏忙循环(⾃旋)⼀段时间,如果在这段时间内能获得锁,就可以避免进⼊阻塞状态

⾃旋锁虽然能避免进⼊阻塞状态从⽽减少开销,但是它需要进⾏忙循环操作占⽤ CPU 时间,它只适⽤于共享数据的锁定状态很短的场景。

在 JDK 1.6 中引⼊了⾃适应的⾃旋锁。⾃适应意味着⾃旋的次数不再固定了,⽽是由前⼀次在同⼀个锁上的⾃旋次数及锁的拥有者的状态来决定

锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进⾏消除。

锁消除主要是通过逃逸分析来⽀持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可
以把它们当成私有数据对待,也就可以将它们的锁进⾏消除。

相对的,对于⼀些看起来没有加锁的代码,其实隐式的加了很多锁。例如下⾯的字符串拼接代码就隐式加了锁:

public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}

String 是⼀个不可变的类,编译器会对 String 的拼接⾃动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作:

public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}

每个 append() ⽅法中都有⼀个同步块。虚拟机观察变量 sb,很快就会发现它的动态作⽤域被限制在 concatString() ⽅法内部。也就是说,sb 的所有引⽤永远不会逃逸到 concatString() ⽅法之外,其他线程⽆法访问到它,因此可以进⾏消除。

锁粗化

如果⼀系列的连续操作都对同⼀个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。上⼀节的示例代码中连续的 append() ⽅法就属于这类情况。如果虚拟机探测到由这样的⼀串零碎的操作都对同⼀个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上⼀节的示例代码就是扩展到第⼀个 append() 操作之前直⾄最后⼀个 append() 操作之后,这样只需要加锁⼀次就可以了。

轻量级锁

JDK 1.6 引⼊了偏向锁和轻量级锁,从⽽让锁拥有了四个状态:⽆锁状态(unlocked)偏向锁状态(biasble)轻量级锁状态(lightweight locked)重量级锁状态(inflated)

下图左侧是⼀个线程的虚拟机栈,其中有⼀部分称为 Lock Record 的区域,这是在轻量级锁运⾏过程创建的,⽤于存放锁对象的 Mark Word。⽽右侧就是⼀个锁对象,包含了 Mark Word 和其它信息。

Java 多线程学习笔记 07-JVM 对锁的优化

轻量级锁是相对于传统的重量级锁⽽⾔,它使⽤ CAS 操作来避免重量级锁使⽤互斥量的开销。对于绝⼤部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使⽤互斥量进⾏同步,可以先采⽤ CAS 操作进⾏同步,如果 CAS 失败了再改⽤互斥量进⾏同步。

当尝试获取⼀个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使⽤ CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。如果 CAS 操作失败了,虚拟机⾸先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进⼊同步块继续执⾏,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争⽤同⼀个锁,那轻量级锁就不再有效,要膨胀为重量级锁。

偏向锁

偏向锁的思想是偏向于让第⼀个获取锁对象的线程,这个线程在之后获取该锁就不再需要进⾏同步操
作,甚⾄连 CAS 操作也不再需要

当锁对象第⼀次被线程获得的时候,进⼊偏向状态,标记为 1 01。同时使⽤ CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进⼊这个锁相关的同步块就不需要再进⾏任何同步操作。当有另外⼀个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。


参考资料:

《Java 编程思想》第 4 版

《深⼊理解 Java 虚拟机》

上一篇:07-数据结构与算法-递归


下一篇:07- Vue3 UI Framework - Switch 组件