[面试题] synchronized 的锁升级、锁优化是怎样的?
📖 版块 A:知识讲解版 (温故知新)
-
核心背景:在 JDK 1.6 之前,synchronized 是重量级锁,依赖操作系统的 Mutex Lock 实现,涉及用户态到内核态的切换,性能较差。1.6 之后引入了偏向锁和轻量级锁,实现了一套“锁升级”机制。
-
锁升级过程(单向不可逆):
-
无锁状态:对象头 Mark Word 初始状态。
-
偏向锁 (Biased Lock):
- 场景:一段同步代码长期只由一个线程访问。
- 原理:在对象头 Mark Word 中记录线程 ID。下次该线程进入时,只需比对 ID,无需 CAS 操作。
-
轻量级锁 (Lightweight Lock):
- 场景:存在少量线程竞争,且同步块执行速度极快。
- 原理:通过 CAS 尝试将对象头 Mark Word 指向线程栈中的 Lock Record。如果失败,则通过“自旋”等待。
-
重量级锁 (Heavyweight Lock):
- 场景:竞争激烈,自旋超过一定次数或已有线程在排队。
- 原理:堆中创建一个 Monitor 对象(ObjectMonitor),未抢到锁的线程进入阻塞状态(WaitSet/EntryList),由操作系统调度。
-
-
其他关键优化手段:
- 自适应自旋 (Adaptive Spinning):自旋的次数不再固定,而是根据上一次在同一个锁上的自旋时间及锁的状态来决定。
- 锁消除 (Lock Elimination):JIT 编译器通过逃逸分析,发现同步块内的变量不会逃逸出方法,则直接取消锁。
- 锁粗化 (Lock Coarsening):如果一系列连续操作都对同一个对象反复加锁解锁(如循环内加锁),JIT 会将锁范围扩大到整个操作序列外部。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:synchronized 的锁升级、锁优化是怎样的?
💬 高手逐字稿:
synchronized 的锁升级是 JVM 为了优化并发性能而设计的一套从低功耗到高可靠性的平滑过渡方案。在 JDK 1.6 之后,synchronized 不再是一上来就请求操作系统级别的重量级锁,而是根据竞争的激烈程度,经历了“无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁”的过程。
首先是偏向锁。JVM 发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。所以它会在对象头的 Mark Word 里记录下这个线程 ID。这个线程下次再来时,直接对比 ID 就能进,几乎没有任何额外开销。
但如果有第二个线程尝试竞争,偏向锁就会撤销并升级为轻量级锁。这时候线程不再只是打个标记,而是会在自己的栈帧里开辟一块叫 Lock Record 的空间,尝试通过 CAS 操作把对象头的 Mark Word 拷贝并指向自己。如果竞争不激烈,线程会通过“自旋”的方式在那里原地待命,避免了线程挂起带来的上下文切换。
最后,如果竞争非常激烈,或者线程自旋次数到了阈值,锁就会膨胀为重量级锁。这时候会创建一个 ObjectMonitor,所有没抢到锁的线程都会被阻塞并放入队列,交给操作系统来调度。虽然性能最重,但它能保证在高并发下不会让 CPU 空转。
在这里我想分享一个容易被忽略的细节: 就是锁升级虽然是不可逆的,但“偏向锁”在现代高版本 JDK(如 JDK 15+)中其实已经被默认禁用甚至废弃了。原因是维护偏向锁撤销的逻辑太复杂,且在现在的云原生、微服务环境下,极少有单线程长久占有锁的情况。
此外,除了锁升级,JVM 还有两个很重要的优化:锁消除和锁粗化。锁消除是基于逃逸分析,如果代码里对局部变量加锁,JIT 发现这变量跑不出这个方法,就会直接把锁删掉;而锁粗化是如果发现你在循环里不停加锁解锁,它会自动把锁的范围扩大到循环外面,减少频繁 CAS 的开销。
所以,synchronized 的性能现在在大多数场景下已经不输于 ReentrantLock 了,除非我们需要公平锁或者 Condition 唤醒这种高级特性,否则 synchronized 是首选。