轻量级锁失败后,到底会不会自旋?——一次被八股文带偏的复盘
起因
前几天复习 Java 锁升级,看到一个问题:
轻量级锁获取失败后,会发生什么?
我脑子里立刻蹦出那句经典的八股文答案:
“轻量级锁会先自旋尝试获取锁,自旋失败后再升级为重量级锁。”
看起来没毛病,网上到处都是这个说法。但当我真正去翻 HotSpot 源码时,发现事情并不是这样。
源码里到底发生了什么
以 JDK 8 的 HotSpot 为例,轻量级锁的获取逻辑在fast_lock里,核心步骤非常干脆:
- CAS 尝试:把对象头的 Mark Word 替换成指向当前线程栈中 Lock Record 的指针。
- 成功:拿到锁,继续执行。
- 失败:直接调用
ObjectSynchronizer::inflate,膨胀成重量级锁。
翻遍这条路径,没有任何自旋逻辑。CAS 一旦失败,就意味着“存在真实竞争”,JVM 不再做无谓的尝试,直接升级。
也就是说:
轻量级锁 CAS 失败 → 直接膨胀为重量级锁,中间没有自旋。
那“自旋”到底在哪?
自旋确实存在,但它属于重量级锁内部的优化,不是轻量级锁的步骤。
当锁已经膨胀成重量级锁(ObjectMonitor)后,线程尝试获取 monitor 时,如果发现锁被占用,ObjectMonitor 的入口逻辑会先让线程短暂自旋(TrySpin_VaryDuration,自适应自旋),希望能抢到刚刚被释放的锁,避免直接park进入内核态阻塞。自旋一段时间仍失败,才真正阻塞。
所以正确的划分是:
| 阶段 | 行为 |
|---|---|
| 轻量级锁 CAS 失败 | 直接膨胀为重量级锁,不自旋 |
| 重量级锁获取失败 | 先自旋,失败再阻塞 |
八股文把这两件事揉在了一起,说成“轻量级锁自旋失败再升级”,顺序和归属都错了。
为什么会流传这个错误说法?
我复盘了一下,大概有几个原因:
“自旋锁”这个概念太深入人心。JDK 1.6 之前自旋锁是一个独立的优化(
-XX:+UseSpinning),很多人一提锁竞争就想到自旋,于是想当然地安到了轻量级锁头上。描述上的偷懒。从线程视角看,“我抢锁失败了,等了一会儿,然后被阻塞”——这个“等了一会儿”很容易被笼统地说成自旋,而不去区分它发生在哪个阶段。
面试八股文的自我复制。一个说法被写进博客、被面试题收录、被更多人背诵,就慢慢变成了“标准答案”,很少有人回去核对源码。
一个补充:新版本 JDK 的变化
值得一提的是,在较新的 JDK(如 JDK 21 及以后)中,轻量级锁的实现有所调整。在LM_LIGHTWEIGHT模式下,CAS 遇到竞争时可能会先短暂自旋一下,目的是避免过早膨胀。但这是新架构下的性能微调,不能拿来解释经典实现(JDK 8 等)的行为。
所以如果你看的是 JDK 8 的源码,结论就是:失败即膨胀,不自旋。
教训
这件事给我最大的提醒是:
八股文和源码事实之间,经常有偏差。
八股文适合快速回忆框架,但一旦涉及“具体在哪一步、发生了什么”,就必须回到源码。尤其是并发这种细节极多的领域,一句笼统的“先自旋再升级”,可能就把两个不同阶段的机制混为一谈。
下次再看到类似的说法,我的第一反应应该是:
- 这个行为发生在哪个阶段?
- 属于哪把锁的机制?
- 源码里真的有这一步吗?
而不是直接背下来。
总结
- 轻量级锁 CAS 失败 →直接膨胀为重量级锁,不自旋。
- 自旋发生在重量级锁的获取入口,是 ObjectMonitor 的优化。
- 八股文“轻量级锁自旋失败再升级”的说法,混淆了阶段和归属。
- 新版本 JDK 有调整,但经典实现(JDK 8)就是“失败即膨胀”。
本文是一次自我纠错的记录。如果你也在复习锁升级,希望它能帮你少走一点弯路。