在 Java 并发编程中,synchronized 锁升级是最容易被误解、却又最影响性能的细节。synchronized 是 Java 最古老也最常用的内置锁,但很多人仍以为它“一用就重、性能差”。事实上,自 JDK 1.6 起,HotSpot 给 synchronized 设计了一套精巧的锁升级机制:对象锁并不是一上来就交给操作系统互斥量,而是沿着“无锁 → 偏向锁 → 轻量级锁 → 重量级锁”逐级膨胀。理解 synchronized 锁升级的四个状态与触发条件,能帮你在高并发场景下做出更优的并发设计,避免把本可以无开销的同步变成内核态阻塞。本文从对象头 Mark Word 入手,配合 JOL 实测代码,把这套机制讲透。
一、为什么需要锁升级
早期版本的 synchronized 直接依赖操作系统的互斥量(mutex),每次加锁、释放都要在用户态与内核态之间切换,开销不可忽视。但 JVM 团队通过大量统计发现:现实里“大多数锁在生命周期内都没有竞争”——同一个线程反复进入同步块(例如单线程复用、可重入),如果每次都走内核锁,纯属浪费。
因此 JDK 1.6 引入了偏向锁与轻量级锁,目标只有一个:在无线程竞争时,让加锁近乎零成本;只有真正出现竞争,才逐步“升级”到更重的实现。这就是 synchronized 锁升级思想的由来——用一层渐进式的代价,换取低竞争场景下的高性能。
二、锁存在哪:对象头与 Mark Word
每个 Java 对象在堆里都有一个对象头(Object Header),其中最重要的部分是 Mark Word,它保存了哈希码、分代年龄、是否偏向,以及锁标志位(lock bits)。在 64 位 JVM 下,Mark Word 占 8 字节,它的含义会随锁状态切换。理解了 Mark Word,就理解了 synchronized 锁升级的“舞台”。
| 锁状态 | 锁标志位 | Mark Word 主要字段 | 加锁开销 |
|---|---|---|---|
| 无锁 | 01(偏向位 0) | hashcode / 分代年龄 | 无 |
| 偏向锁 | 01(偏向位 1) | 偏向线程 ID + epoch | 极低 |
| 轻量级锁 | 00 | 指向栈帧 Lock Record 的指针 | 低(CAS + 自旋) |
| 重量级锁 | 10 | 指向 ObjectMonitor 的指针 | 高(内核阻塞) |
我们可以借助 OpenJDK 的 JOL(Java Object Layout)工具,把对象在加锁前后的布局打印出来,亲眼看到 Mark Word 的变化:
import org.openjdk.jol.info.ClassLayout;
public class MarkWordDemo {
public static void main(String[] args) throws Exception {
Object lock = new Object();
// 未加锁:无锁状态,Mark Word 存 hashcode / 分代年龄
System.out.println("before: " + ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
// 加锁后:偏向锁或轻量级锁,Mark Word 含义改变
System.out.println("inside: " + ClassLayout.parseInstance(lock).toPrintable());
}
// 释放后:回到无锁(若已膨胀为重量级则不会退化)
System.out.println("after: " + ClassLayout.parseInstance(lock).toPrintable());
}
}
三、四种锁状态与升级路径
synchronized 锁升级完整覆盖四种状态,各自的适用场景与实现方式如下:
| 状态 | 适用场景 | 实现方式 | 竞争处理 |
|---|---|---|---|
| 无锁 | 尚未有任何线程加锁 | 普通对象头 | — |
| 偏向锁 | 同一线程反复进入同步块 | Mark Word 记录偏向线程 ID | 比对线程 ID,免 CAS |
| 轻量级锁 | 多线程交替进入、无同时竞争 | 栈帧 Lock Record + CAS | CAS 失败则自旋 |
| 重量级锁 | 真实高并发竞争 | ObjectMonitor 互斥量 | 未抢到锁的线程 park 阻塞 |
锁升级是单向的:无锁 →(第一个线程进入)偏向锁 →(有别的线程来竞争)轻量级锁 →(自旋失败 / 竞争激烈)重量级锁。个别情况下偏向锁被撤销后会短暂回退到无锁,但重量级锁一旦进入就不会再降级。
四、偏向锁:单线程的“免检通道”
偏向锁的核心思想是:如果一段同步代码一直只被同一个线程执行,那这把锁就成了该线程的“专属通道”。后续进入时,JVM 只需比对 Mark Word 里的偏向线程 ID 是否等于自己,连一次 CAS 都不用做,单线程复用的开销几乎为零。
但偏向锁也有代价:一旦有第二个线程尝试进入,JVM 必须在全局安全点(Stop-The-World)撤销偏向,把锁恢复到可竞争状态。这个撤销成本在多线程频繁竞争时反而拖累性能。也正因如此,从 JDK 15 开始,偏向锁默认被禁用(-XX:+UseBiasedLocking 已废弃并在后续版本移除)。理解它,有助于看懂“为什么现在默认没有偏向锁”,以及历史代码里残留的调优参数为何失效。
五、轻量级锁:用自旋避免内核切换
当偏向锁被撤销、或对象一开始就未偏向,但有线程要进入同步块时,JVM 会尝试轻量级锁:在当前线程的栈帧里创建一个 Lock Record,把对象 Mark Word 复制到其中(Displaced Mark Word),然后用 CAS 把对象头的 Mark Word 替换为“指向该 Lock Record 的指针”。
CAS 成功则本线程获得轻量级锁;CAS 失败说明有别的线程已经在竞争,JVM 会让当前线程自旋(原地忙等)少量次数,期待对方很快释放;若自旋仍失败、或自旋次数耗尽,锁就会膨胀为重量级锁。轻量级锁的优势是:竞争不激烈时,线程没有进入内核阻塞,只是原地自旋,避免了上下文切换——前提是你得保证临界区足够短、释放够快。
六、重量级锁:操作系统互斥量
一旦升级到重量级锁,JVM 会在堆外创建一个 ObjectMonitor(监视器),其中包含等待队列(cxq、EntryList)与所有者线程指针(_owner)。未抢到锁的线程会被 park() 挂起,进入内核态阻塞,等待被唤醒;释放锁时再通过 unpark() 唤醒等待者。
这也是为什么“长临界区 + 高竞争”下 synchronized 会变慢:大量线程在用户态与内核态之间被挂起、唤醒,上下文切换成本陡增。可用 jstack 观察线程状态为 BLOCKED (on object monitor),或用 async-profiler 火焰图查看 monitorenter / monitorexit 占比。
"t-1" #13 prio=5 os_prio=0 cpu=12.30ms
java.lang.Thread.State: BLOCKED (on object monitor)
at EscalationDemo.lambda$main$0(EscalationDemo.java:9)
- waiting to lock <0x000000076b6a3c20> (a java.lang.Object)
- locked <0x000000076b6a3c20> (a java.lang.Object)
上面这段 jstack 输出正是重量级锁的特征:线程被挂起在内核态,等待监视器所有权,这正是 synchronized 锁升级到最重一档时的真实表现。
七、代码实测:看 Mark Word 如何变化
下面用 JOL 在多个线程同时竞争同一把锁时打印对象布局,可以观察到 Mark Word 从偏向 / 轻量态跳到重量级(标志位从 01 / 00 变为 10,并指向 ObjectMonitor):
import org.openjdk.jol.info.ClassLayout;
public class EscalationDemo {
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 4; i++) {
new Thread(() -> {
synchronized (lock) {
// 多个线程同时抢同一把锁,触发膨胀为重量级
System.out.println(
Thread.currentThread().getName() + " holds:
" +
ClassLayout.parseInstance(lock).toPrintable());
}
}, "t-" + i).start();
}
}
}
提示:稳定的重量级观察需要制造“同时进入”的竞争(可用 CountDownLatch 让线程在同一时刻抢锁),单次运行中偏向 / 轻量级切换也可能出现,多跑几次即可看到完整升级链路。
八、性能建议与常见误区
| 常见误区 | 事实 |
|---|---|
| synchronized 一定慢 | 低竞争下经过锁升级优化,与 ReentrantLock 差距极小,JIT 还能做锁消除、锁粗化 |
| 偏向锁永远好 | JDK 15+ 默认禁用,现代多核竞争激烈时反而有害 |
| 加锁范围越大越安全 | 同步块越大竞争窗口越长,越容易升级为重量级锁 |
基于 synchronized 锁升级的机制,给出几条可落地的优化建议:
- 缩小同步块:只包住真正共享可变状态的几行,减少竞争窗口。
- 锁分解 / 锁分段:把一把大锁拆成多把(参考 ConcurrentHashMap 的分段思想),降低单锁竞争。
- 读写分离:读多写少用 StampedLock 或 ReentrantReadWriteLock,避免读之间互斥。
- 计数类场景:用 LongAdder 替代 synchronized++ 或 AtomicLong,内部分段减少争用。
- 绕开锁:若临界区极短且无真实共享,可借助局部变量、ThreadLocal,或用 CompletableFuture 异步编程 编排任务,从设计上消除竞争。
九、小结
synchronized 锁升级是 JVM 在“正确性”与“性能”之间做的精细权衡:无竞争时几乎免费,有竞争时再付出代价。掌握对象头 Mark Word、偏向 / 轻量级 / 重量级三种实现,以及它们各自的适用场景,你就能在并发代码里做出更理性的取舍——该缩小同步块就缩小,该换无锁结构就换。想进一步理解 Java 并发的可见性基础,可以复习 Java 内存模型 JMM 与 volatile;高并发吞吐则可结合 Java 虚拟线程 重新思考线程模型。




