Java 在并发场景下,为了兼顾性能与安全,引入了锁升级机制。同一把锁会根据竞争情况,从低开销状态逐步升级到高开销状态,以减少无意义的阻塞与线程切换。
Java 对象的锁标志是存储在对象头中的 Mark Word (在 64 位虚拟机中长度是 64 位)中的:
倒数第3位:表示该对象是否可偏向;
倒数两位:锁标志位(Lock Flag)。
一、对象创建:可偏向状态
在 JDK 6 之后默认开启偏向锁,且过了启动延迟(默认 4 秒,可通过 -XX:BiasedLockingStartupDelay=0
取消延迟)后,新创建的对象在 Mark Word 中的最后三位标志位是 101
,表示 可偏向(Biased)。此时对象还没有被加锁,Mark Word 中:
线程 ID(54 位):为空(未偏向到具体线程);
Epoch/GC 年龄等(7 位):GC 用于年龄计数;
标志位(3 位):101,偏向锁标记
二、第一次加锁:进入偏向锁
当某个线程第一次对这个对象执行 synchronized
时:
JVM 会把该线程的 ID 写入对象的 Mark Word。
标志位依旧是
101
,但状态变为“已偏向到某个线程”。
此时加锁/解锁都不需要 CAS 操作,只需判断对象头中的线程 ID 是否等于当前线程 ID,非常快。
相等:直接进入同步块(无额外开销);
不等:意味着有其他线程竞争,需要进入下一步处理。
三、其他线程竞争:撤销偏向锁 → 无锁状态
如果有另一个线程尝试获取这把锁,JVM 会:
暂停持有锁的线程(安全点):JVM 在执行某些全局性操作(如 GC、偏向锁撤销)时,需要所有线程都到达“可安全暂停”的位置,这些位置称为安全点。暂停后可以安全地修改对象头信息而不影响正在执行的线程。
撤销偏向锁:清除 Mark Word 中的线程 ID,修改标志位为
001
(无锁,可偏向标志位为 0);
这样,对象就回到了无锁状态。偏向锁只适合单线程反复加锁解锁的场景,一旦出现竞争,保留线程 ID 已经没有意义。
四、无锁状态加锁:轻量级锁
当对象处于无锁状态时,如果有线程执行 synchronized
:
线程会在栈帧中创建 锁记录(Lock Record),把对象头中的 Mark Word 复制到锁记录中;
使用 CAS + 自旋 尝试将对象的 Mark Word 替换为指向锁记录的指针。
CAS 成功:获得 轻量级锁;
CAS 失败(默认 10 次失败):说明有其他线程持有锁,会升级为重量级锁并阻塞线程,避免 CPU 空转浪费。
五、轻量级锁竞争:升级为重量级锁
如果轻量级锁的 CAS 失败(意味着已经有其他线程持有锁),JVM 会将锁升级为 重量级锁(Monitor):
对象 Mark Word 存储的是指向 Monitor 对象的指针。
竞争失败的线程会调用
park()
挂起,进入 Monitor 的阻塞队列(EntryList)。持有锁的线程释放锁时,会调用
unpark()
唤醒等待队列中的线程。
重量级锁保证了高竞争下的安全性,但会涉及用户态/内核态切换和线程调度,性能开销最大。
补充:JDK 15 移除偏向锁
在 JDK 15 之后取消了偏向锁,第一次加锁直接走轻量级锁流程(CAS 尝试获取锁)。主要是因为偏向锁的收益越来越低,而维护它的复杂度和成本反而不小。
偏向锁在 JDK 6 引入时,是为了优化同一线程反复获取同一把锁的场景。但是现在的服务端、云原生应用几乎都是高并发场景,锁一开始就会有竞争,偏向锁还没发挥作用就被撤销了。撤销偏向锁是有成本的(安全点、批量重偏向/撤销),在竞争场景下反而变成性能负担。
轻量级锁、锁消除等优化手段已经足够应对低竞争场景,维护偏向锁的复杂逻辑不再划算,因此 JDK 15 默认关闭并废弃,JDK 18 彻底移除。