" />
文章

synchronized 锁升级过程:从偏向锁到重量级锁

synchronized 锁升级过程:从偏向锁到重量级锁

面试指北 · 去哪儿

题意

面试官问「synchronized 锁升级过程」,通常不是想听你背四个名词,而是想确认几件事:

  1. synchronized 在 JVM 里靠什么实现?对象头里的 Mark Word 到底存了什么?
  2. 无锁、偏向锁、轻量级锁、重量级锁,分别适合什么竞争场景?
  3. 锁状态之间怎么迁移?触发条件是什么?
  4. 锁能不能降级?JDK 版本演进后有什么变化?
  5. 它和 CAS、ObjectMonitor、ReentrantLock 是什么关系?

所以回答时最好先给主线,再补细节,最后主动点出常见误区。

结论

先给结论:

  • synchronized 的锁状态记录在对象头 Mark Word 中,重量级锁阶段配合 HotSpot 的 ObjectMonitor 实现。
  • 主流描述是四级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,竞争越激烈越往后升级。
  • 偏向锁适合「同一个线程反复进入」;轻量级锁适合「多线程交替进入、碰撞不多」;重量级锁适合「真竞争、需要阻塞排队」。
  • 锁升级基本是单向的,HotSpot 没有常规意义上的「重量级锁降级为轻量级锁/偏向锁」。
  • JDK 15 起偏向锁默认禁用,后续版本已移除相关实现,所以新版本面试可以补一句「偏向锁已经是历史机制」。

一句话版:偏向锁赌没有竞争,轻量级锁用 CAS 赌竞争很短,重量级锁用 ObjectMonitor 兜底真竞争;锁升级不可逆。

无锁 --(第一次加锁,记录线程 ID)--> 偏向锁
偏向锁 --(另一线程竞争,撤销偏向)--> 轻量级锁
轻量级锁 --(CAS 失败且自旋失败,膨胀)--> 重量级锁

要点展开

1. 对象头与 Mark Word:锁状态到底存在哪

Java 对象在内存里大致分三块:对象头、实例数据、对齐填充。对象头里最关键的是 Mark Word,它只有机器字长那么大(32 位 JVM 是 32 bit,64 位 JVM 是 64 bit),却要复用存储 hashCode、分代年龄、锁标志位、偏向线程 ID 等信息。因为空间有限,不同锁状态下 Mark Word 的布局不同,这就是「锁升级」能发生的基础。

可以记一个简表(64 位 HotSpot 常见布局,忽略压缩指针等细节):

锁状态 Mark Word 标志位 Mark Word 主要存什么
无锁 01(偏向位 0) hashCode、分代年龄、锁标志位
偏向锁 01(偏向位 1) 偏向线程 ID、epoch、分代年龄
轻量级锁 00 指向线程栈中 Lock Record 的指针
重量级锁 10 指向 ObjectMonitor 的指针
GC 标记 11 与 GC 相关

注意:偏向锁的标志位也是 01,靠「是否偏向」那一位区分无锁和偏向锁。原文里「无锁 01、轻量级 00、重量级 10」没问题,但不要误以为 01 只代表无锁。

2. 无锁 → 偏向锁:赌只有一个线程

偏向锁的设计目标很朴素:大多数同步块其实从头到尾只有一个线程在跑,如果每次进出都做 CAS,纯属浪费。于是 JVM 让第一次加锁的线程把「身份」记在对象头上,后续这个线程再来,只要对一下线程 ID 就放行。

流程大致是:

  1. 线程第一次进入同步块,发现对象处于可偏向状态。
  2. 通过 CAS 把当前线程 ID 写进 Mark Word,偏向位设为 1。
  3. 同一线程再次进入,检查 Mark Word 里的线程 ID 是不是自己,是就直接进入,不再 CAS。
  4. 如果另一个线程来抢,偏向锁失效,进入撤销/升级流程。

偏向锁的撤销不是「改个标志位」那么轻量。它通常需要等到全局安全点(safepoint),暂停持有偏向锁的线程,检查该线程是否还活着、是否还在同步块里。如果线程已经退出或者对象不再被使用,可能撤销为无锁,或者批量重偏向给新线程;如果还在用,就升级为轻量级锁。也正因为撤销成本不低,JDK 15 起默认关闭了偏向锁。

这里有个容易忽略的点:对象一旦调用 Object.hashCode() 或 System.identityHashCode(),Mark Word 要存 hash,偏向锁往往会被撤销。因为偏向锁的 Mark Word 里没有多余空间同时放线程 ID 和 hash。面试里如果能补这一句,会显得不是死背流程。

3. 偏向锁 → 轻量级锁:多线程交替,但别撞太久

当另一个线程尝试获取已经被偏向的锁,偏向锁被撤销,竞争线程按轻量级锁的方式加锁。轻量级锁的核心思想是:不急着让操作系统挂起线程,先用 CAS 在用户态试一试。

加锁流程:

  1. 当前线程在自己的栈帧里创建一个 Lock Record(锁记录)。
  2. 把对象头 Mark Word 拷贝到 Lock Record 的 Displaced Mark Word 字段里,相当于留个备份。
  3. 用 CAS 尝试把对象头 Mark Word 更新为指向这个 Lock Record 的指针。
  4. CAS 成功:当前线程拿到轻量级锁,对象头标志位变为 00。
  5. CAS 失败:先判断是不是自己重入。如果是重入,把 Lock Record 的 Displaced Mark Word 置为 null 表示重入;如果不是重入,说明有竞争,进入自旋。
  6. 自旋一段时间还拿不到锁,锁膨胀为重量级锁。

解锁流程也围绕 CAS:

  • 用 CAS 把 Displaced Mark Word 恢复回对象头。
  • 如果恢复成功,说明没有膨胀,轻量级锁释放完成。
  • 如果恢复失败,说明锁已经膨胀成重量级锁,需要走 ObjectMonitor 的释放逻辑,并唤醒等待线程。

轻量级锁适合「两个线程交替执行」的场景:没有真正同时争抢,CAS 很快成功,避免了内核态阻塞。但如果是高并发同时抢,自旋会白烧 CPU,最后还是得膨胀。

4. 轻量级锁 → 重量级锁:真竞争就交给操作系统

当轻量级锁 CAS 反复失败、自旋也拿不到锁,JVM 认为竞争已经比较激烈,就把锁膨胀为重量级锁。此时 Mark Word 的标志位变成 10,指向一个 ObjectMonitor 对象。

ObjectMonitor 是 HotSpot 用 C++ 实现的监视器,里面有几个关键结构:

  • _owner:当前持有锁的线程。
  • _EntryList:竞争锁但还没拿到的线程队列。
  • _WaitSet:调用 wait() 后进入等待的线程集合。
  • _cxq:竞争队列,配合 EntryList 做调度。

重量级锁的「重」在于:拿不到锁的线程会被挂起(park),从用户态切到内核态,释放锁时再唤醒。线程上下文切换的成本远高于一次 CAS,所以它只适合真正高竞争的场景。也正因为有 ObjectMonitor,wait/notify/notifyAll 只能在 synchronized 里用——这些方法操作的就是监视器的 WaitSet。

5. 锁降级:这里要纠正一个常见说法

原文提到「重量级锁可以降级为偏向锁或轻量级锁」,这个说法在 HotSpot 的 synchronized 上是不准确的。HotSpot 的锁升级基本不可逆:一旦膨胀为重量级锁,不会因为后来竞争变小就自动降回轻量级锁或偏向锁。对象被 GC 回收后重新分配,那是另一个对象的故事,不是同一把锁的降级。

容易混淆的有两个东西:

  • 偏向锁的批量重偏向/批量撤销:这是 JVM 对一批对象做的优化,不是重量级锁降级。
  • 读写锁的锁降级:ReentrantReadWriteLock 支持写锁降级为读锁,StampedLock 也有类似概念,但那是另一套 API,不要和 synchronized 混在一起。

所以面试时如果被问「锁能不能降级」,稳妥回答是:synchronized 的锁升级是单向的,HotSpot 没有运行期降级机制;偏向锁上的批量操作不是降级。

6. 顺带要会的 JIT 优化

锁升级是 JVM 的运行时策略,另外还有几个编译期/运行期优化经常一起问:

  • 锁消除:JIT 通过逃逸分析发现锁对象不会逃出当前线程,直接去掉同步。
  • 锁粗化:一连串相邻的同步块,JIT 可能合并成一个大同步块,减少反复加解锁。
  • 自适应自旋:自旋次数不是固定的,JVM 会根据上一次在同一个锁上的成功率动态调整。

这些和「偏向锁 → 轻量级锁 → 重量级锁」不是同一个层次的东西,但面试里经常连着问,能区分开是加分项。

易错点与对比

  1. 偏向锁标志位不是 00。 偏向锁和无锁都是 01,靠偏向位区分;00 是轻量级锁,10 是重量级锁。
  2. 锁升级不可逆。 不要答「竞争降低后重量级锁降回轻量级锁」。
  3. 偏向锁不是「不加锁」。 它只是把加锁简化为线程 ID 比对,撤销时仍要 safepoint。
  4. 轻量级锁不是「无锁」。 它仍然通过 CAS 修改对象头,只是不阻塞线程。
  5. 重量级锁不是「锁本身更重」。 它是在高竞争下用线程阻塞换 CPU 空转的兜底方案。
  6. wait/notify 会涉及重量级锁。 调用 wait 的线程进入 ObjectMonitor 的 WaitSet,不是轻量级锁能完成的。
  7. JDK 版本要提。 JDK 15 起偏向锁默认禁用,新版本已移除相关实现;老版本可用 -XX:+UseBiasedLocking 观察。
  8. 和 ReentrantLock 对比:synchronized 是 JVM 关键字,自动释放锁,非公平,配合 ObjectMonitor 支持 wait/notify;ReentrantLock 是 JDK 层 API,需要手动 unlock,支持公平锁、可中断、多条件队列、tryLock。锁升级是 synchronized 特有的优化路径,ReentrantLock 基于 AQS 的 CAS + 队列,不是同一套机制。

小结

把回答串成一条线:

  1. synchronized 的锁状态存在对象头 Mark
License:  CC BY 4.0