偷偷告诉你,synchronized不再使用偏向锁啦

530 阅读4分钟

Synchronized 底层优化是Java面试常考察的点。简略来说,Synchronzied 采用逐步升级的加锁机制来降低加锁成本,每次加锁失败就升级竞争下一级锁,每升高一级,加锁成本就会提高。

在 Java 中,加锁就是线程用锁对象记录自己的一个标记。当其他线程加锁扫描到锁对象已被其他线程标记,就会加锁失败。不再执行后续临界区的代码。

一、Synchronized锁升级过程\color{blue}{一、Synchronized 锁升级过程}

Synchronized 首先尝试获取偏向锁(Biased lock),偏向锁会把对象头中的线程编号字段指向自己,表示锁对象被该线程标记。如果指向失败,则撤销偏向锁,升级为轻量级锁(Lightweight Lock)。这时线程使用 CAS 把对象头交换到自己的栈。如 CAS 失败,会进行重试(即自旋 Spin),重试多次仍然失败,升级到重量级锁(Heavyweight Lock),交给 OS 内核进行最终的同步处理。

锁升级机制

二、偏向锁被废弃\color{blue}{二、偏向锁被废弃}

偏向锁只需要设置对象头的几个字段的值就能完成加锁,“性能很高”。但在 JDK15 中,偏向锁被默认关闭。在 JDK18 中,更被标记为废弃,并不再允许通过命令行手动开启。
看起来效果很好的偏向锁,为什么逐步被打入“冷宫”?OpenJDK 开发团队在 JEP 374 中解释:

过去看到的性能提升如今远没有那么明显。许多受益于偏向锁的程序都是较旧的遗留程序,它们使用早期的 Java 集合 API,它们在每次访问时进行同步(例如 Hashtable 和 Vector )。对于较新的程序,针对单线程场景通常使用 Java 1.2 中引入的非同步集合(例如 HashMap 和 ArrayList ),针对多线程场景,则使用 Java 5 引入的性能更高的并发结构。这意味着,如果代码升级到这些较新的类,相比偏向锁,会有更大的性能提升。此外,围绕线程池队列和工作线程构建的应用程序通常在禁用偏向锁定的情况下性能更好。(例如,SPECjbb2015 就是这样设计的,而 SPECjvm98 和 SPECjbb2005 则不是)。
偏向锁定的代价是在发生锁争用时需要执行昂贵的撤销操作。因此,受益于它的程序只是那些无竞争同步操作的程序。偏向锁的高效是假定在执行简单的锁检查加上偶尔昂贵的撤销成本,仍然低于执行 CAS 指令的成本。但 HotSpot 已经发生了很大的变化,原子指令成本的变化也改变了保持该关系所需的无竞争操作的数量。
另一个值得注意的方面是,当同步操作上花费的时间只占程序总工作负载的一小部分时,即使先前的成本关系成立,程序也不会从偏向锁中获得明显的性能改进。

三、废弃原因分析\color{blue}{三、废弃原因分析}

综合来看,偏向锁被废除主要有下面的原因:

3.1 可维护性差

JEP 374 中写到:

偏向锁在同步子系统中引入了大量复杂的代码,并且还会侵入其他 HotSpot 组件

为了实现偏向锁,在 JVM 中引入了大量代码。并且,一个理想的代码库,应该做到“高内聚、低耦合”,但偏向锁的代码和各个模块交叉耦合,相互影响。复杂的偏向锁实现给 OpenJDK 开发者造成了很大的负担,最终让他们不得不考虑放弃。

3.2 优化效果不好

OpenJDK 开发者统计到同步操作在程序实际运行中消耗的资源较少,即使偏向锁有一定提升,但对总体性能影响不大。另外,随着 JDK 并发数据结构(ConcurrentHashMap、CopyOnWriteArrayList等)和 CAS 操作的性能提升,偏向锁的作用很有限

3.3 前景不好

偏向锁优化效果不好,并且也没看到未来的优化空间。没有产出,也没有预期的产品很难长期存在。

四、参考资料\color{blue}{四、参考资料}