synchronized 锁升级全流程解析
在 Java 开发者的眼中,synchronized 曾经是"笨重"的代名词。早期版本的 synchronized 一上来就直接调用操作系统的底层同步机制,导致线程上下文切换频繁,效率极低。
但在 Java 6 之后,HotSpot 虚拟机为了优化性能,对 synchronized 引入了极其精妙的"锁升级"机制。今天,我们就通过深度分析对象头、CAS 竞争和 Monitor 机制,彻底讲透锁升级的原理。
1. 这篇文章要解决什么问题?
早期的 synchronized 只有 重量级锁 一种形式。每当一个线程请求锁,都会触发:
- 用户态与内核态的切换。
- 线程的挂起与唤醒(操作系统层面)。
这种操作极其耗时。但在实际业务中,科学家们发现:
- 很多时候,锁其实只会被 同一个线程多次访问。
- 即使有竞争,往往也是 交替执行,竞争通过简单的自旋就能解决。
锁升级的目的,就是为了根据实际的竞争激烈程度,逐步"按需加重"锁的开销。
2. 核心原理:对象头里的"秘密花园"
要理解锁升级,必须先看懂 Java 对象头(Object Header) 。每个 Java 对象在内存中都带有一个"头",其中的 Mark Word 是控制锁状态的核心。
Mark Word 的动态平衡
在 64 位 JVM 中,Mark Word 占 8 个字节(64 bit)。它是一个"变色龙",在不同的锁状态下,位域的含义完全不同:
| 锁状态 | 25 bit | 31 bit | 1 bit | 4 bit | 1 bit (偏向位) | 2 bit (锁标志位) |
|---|---|---|---|---|---|---|
| 无锁 | 未使用 | hashCode | 0 | 分代年龄 | 0 | 01 |
| 偏向锁 | ThreadID (54bit) | Epoch (2bit) | 0 | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录 (Lock Record) 的指针 | 00 | ||||
| 重量级锁 | 指向互斥量 (Monitor) 的指针 | 10 |

3. 流程/机制描述:一步步进阶的锁
第一阶段:偏向锁 (Biased Locking) ------ 一个人的舞台
核心逻辑:如果锁总是被同一个线程获取,那么标记一下线程 ID 就行了,连 CAS 都不需要。
- 动作:当线程第一次访问同步块,Mark Word 会记录下当前线程 ID,偏向位置为 1。
- 进入:下次该线程再来,只需检查 ThreadID 是否匹配,匹配直接通过。
- 撤销:当有另一个线程尝试竞争时,偏向锁会被撤销,根据对象是否仍锁住决定升级。
第二阶段:轻量级锁 (Lightweight Locking) ------ 彬彬有礼的竞争
核心逻辑:当出现了多个线程交替访问,但没有激烈竞争时,使用 CAS 替换对象头。
- 动作:线程在自己的栈帧中开辟空间(Lock Record),并将对象的 Mark Word 拷贝过去。
- CAS 争夺:线程尝试用 CAS 将对象头的 Mark Word 替换为指向自己栈中 Lock Record 的指针。
- 自旋:如果 CAS 失败,说明有轻微竞争,线程不会挂起,而是"原地踏步"(自旋)一会儿继续尝试。
第三阶段:重量级锁 (Heavyweight Locking) ------ 实打实的冲突
核心逻辑:当自旋次数过多,或者多个线程同时激烈争夺时,锁变得极其沉重。
- 升级触发:轻量级锁 CAS 失败次数达到限度,或者同时有多个线程在自旋等待。
- Monitor 介入 :锁膨胀为重量级锁,Mark Word 指向
ObjectMonitor对象。 - 挂起 :除了持有锁的线程,其它线程全部进入
WaitSet或EntryList等待区,被操作系统挂起,进入阻塞状态。

4. 关键代码/示例
字节码维度的 synchronized
通过 javap -c 观察代码,你会发现同步块是由配对的指令控制的。
java
public class SyncExample {
public void syncBlock() {
synchronized (this) {
// 业务逻辑
}
}
}
对应的字节码摘要:
text
0: aload_0
1: dup
2: astore_1
3: monitorenter // 代表锁的开始
...
15: monitorexit // 代表正常退出
...
21: monitorexit // 代表异常退出(确保锁一定释放)
如何观察对象头?
在实际工作中,我们可以使用 JOL (Java Object Layout) 工具来实时观察 Mark Word 的位变化。
java
import org.openjdk.jol.info.ClassLayout;
/**
* 使用 JOL 观察对象头锁标志位
*/
public class JOLDemo {
public static void main(String[] args) {
Object obj = new Object();
// 1. 无锁状态
System.out.println("--- 无锁状态 ---");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
// 2. 这种情况下可能是偏向锁或轻量锁(取决于 JVM 启动参数)
System.out.println("--- 锁住状态 ---");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
注:由于 JVM 默认偏向锁有 4s 延迟,直接运行可能先看到轻量级锁。
5. 常见误区
误区 1:锁升级是可逆的
反驳 :在 HotSpot 虚拟机中,锁升级通常是 单向不可逆 的(偏向锁 -> 轻量级锁 -> 重量级锁)。虽然在偏向锁撤销后可能重新进入偏向状态,但轻量锁一旦升级为重量锁,通常不会再自动"降级"回去。
误区 2:自旋锁就是轻量级锁
反驳 :自旋(Spin)只是一种 手段,它发生在轻量级锁升级为重量级锁的过程中。轻量级锁本身是利用 CAS 替换指针,而当 CAS 失败时,才会开启自旋优化以期不挂起线程。
6. 实际工作中怎么用?
-
预估并发压力 : 如果你的场景天生就是极高并发、大量争抢(如秒杀核心逻辑),
synchronized可能会迅速膨胀为重量级锁。此时可以考虑ReentrantLock提供的更丰富的 API。 -
JVM 调优建议:
- 偏向锁延迟 :默认 4 秒延迟开启。如果你的应用一启动就有大量线程竞争,可以考虑通过
-XX:BiasedLockingStartupDelay=0来关闭延迟,或者彻底禁用偏向锁。 - 现代趋势 :值得注意的是,JDK 15 之后已经默认禁用了偏向锁,因为维护偏向锁撤销的成本在现代多核架构下有时反而得不偿失。
- 偏向锁延迟 :默认 4 秒延迟开启。如果你的应用一启动就有大量线程竞争,可以考虑通过
总结
synchronized 的设计哲学体现了 Java 性能优化的核心思想:平路加速,上坡减挡。在没有竞争时追求极致性能,在竞争激烈时保证结果正确。理解锁升级,是你通向 Java 并发架构师的必经之路。