前言
偏向锁是从 JDK1.6 引入的一种针对 synchronized 的锁优化技术,然而从 JDK 15 开始,这一特性被官方标记为废弃状态,如果还想继续使用的话需要通过 JVM 参数手动启用。
-XX:+UseBiasedLocking
那么问题来了,JDK15 为什么要废弃偏向锁呢?
什么是偏向锁?
在回答 Why 之前要先明白 What:什么是偏向锁?
在 Java 中可以使用 synchronized 来保证代码块同一时间只被一个线程访问(互斥),它是基于 Monitor Object 模式来实现的。
int i;
public synchronized void mutex() {
i++;
}
public void mutext2() {
synchronized(this) {
i++;
}
}
当线程请求进入临界区时都需要先获取一个 monitor 对象(类似于准入许可证),获取 monitor 对象是通过 compare-and-swap (CAS) 操作来实现的。
从 CPU 的角度来看,CAS 其实是一个开销很昂贵的操作,有没有什么方法可以避免呢?
我们先看一个 JAVA 团队观察到的现象:
在大多数对象的生命周期内,基本上只会有一个线程访问临界区
基于此可以得出一个优化方案:当某个线程首次访问临界区时记录下该线程的信息,当再有线程访问该临界区时判断是否是首次访问的线程
- 如果是:就直接放行,这样就避免了通过 CAS 获取 monitor 的操作
- 如果不是:就升级为轻量级锁
这个优化方案其实就是偏向锁了,在 JVM 的实际实现中,锁升级其实有 4 个状态,并且是只可升级不可降级。
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
为什么要废弃?
大人,时代变了
在过去,Java 应用通常使用的都是 HashTable、Vector 等比较老的集合库,这类集合库大量使用了 synchronized 来保证线程安全。
如果在单线程的情景下使用这些集合库就会有不必要的加锁操作,从而导致性能下降。
而偏向锁可以保证即使是使用了这些老的集合库,也不会产生很大的性能损耗,因为 JVM 知道访问临界区的线程始终是同一个,也就避免了加锁操作。
这一切都很美好,但是随着时代的变化,新的 Java 应用基本都已经使用了无锁的集合库,比如 HashMap、ArrayList 等,这些集合库在单线程场景下比老的集合库性能更好。
即使是在多线程场景下,Java 也提供了 ConcurrentHashMap、CopyOnWriteArrayList 等性能更好的线程安全的集合库。
综上,对于使用了新类库的 Java 应用来说,偏向锁带来的收益已不如过去那么明显,而且在当下多线程应用越来越普遍的情况下,偏向锁带来的锁升级操作反而会影响应用的性能。
时代变了,成本却还在增加
在废弃偏向锁的提案 JEP374 中还提到了与 HotSpot 相关的一点
Biased locking introduced a lot of complex code into the synchronization subsystem and is invasive to other HotSpot components as well.
简单翻译就是偏向锁为整个「同步子系统」引入了大量的复杂度,并且这些复杂度也入侵到了 HotSpot 的其它组件。
这导致了系统代码难以理解,难以进行大的设计变更,降低了子系统的演进能力,
总结下来其实就是 ROI (投资回报率)太低了,考虑到兼容性,所以决定先废弃该特性,最终的目标是移除它。
后续如何兼容?
默认禁用偏向锁可能会导致一些 Java 应用的性能下降,所以 HotSpot 提供了显示开启偏向锁的命令
# 在 Java15 后,手动开启偏向锁在启动的时候会收到警告信息
-XX:+UseBiasedLocking
以下和偏向锁相关的命令参数仍然可以使用,但是虚拟机会列出对应的警告信息表示其已被废弃掉
-XX:BiasedLockingStartupDelay=__
-XX:BiasedLockingBulkRebiasThreshold=__
-XX:BiasedLockingBulkRevokeThreshold=__
-XX:BiasedLockingDecayTime=__
// 必须在 C2 下才能使用
-XX:+UseOptoBiasInlining
// 必须配合参数-XX:+UnlockDiagnosticVMOptions使用
// 并且只能加在其后才能生效
-XX:+PrintBiasedLockingStatistics
-XX:+PrintPreciseBiasedLockingStatistics
so,扶我起来,我还能学!