在 Java 并发编程中,synchronized 曾被视为“重量级锁”的代名词。然而,随着 JDK 1.6 的优化,它已经变身为一把“智能锁”。它不再是一上来就请求操作系统介入,而是学会了根据竞争的激烈程度,动态调整自己的策略。
本文将深入剖析 synchronized 的底层原理,揭示它如何通过对象头(Mark Word) 、Monitor(管程)以及锁升级机制,在性能与安全性之间找到完美的平衡点。
一、 核心基石:对象头与 Monitor
1. 对象的“记分板”:Mark Word
Java 中的每一个对象,在内存中都包含一个对象头(Object Header) 。其中最核心的部分叫做 Mark Word。
Mark Word 就像是一个“多功能显示屏”,它会根据锁的状态不同,复用存储空间,记录不同的信息:
- 哈希码(HashCode)
- GC 分代年龄
- 锁状态标志位
- 关键指针(这是锁升级的核心线索)
2. 幕后管理者:Monitor(管程)
synchronized 在字节码层面对应的是 monitorenter 和 monitorexit 指令。这两个指令背后的实现依赖于操作系统的 Monitor(管程) 模型。
在 JVM(HotSpot)中,Monitor 是由 C++ 实现的 ObjectMonitor 对象。它主要包含三个核心区域:
- _owner:当前持有锁的线程。
- _EntryList(入口等待队列) :所有没抢到锁,正在排队等待的线程。
- _WaitSet(条件等待队列) :获取了锁但因条件不满足(调用
wait())而释放锁、主动挂起的线程。
二、 锁的升级:一场关于“身份标识”的演变
锁升级的本质,就是 Mark Word 中那个“指针”含义的变化。它代表了锁的控制权归属,也代表了竞争的激烈程度。
锁的状态遵循:无锁 → 偏向锁 → 轻量级锁 → 重量级锁 的单向升级路径。
第一阶段:偏向锁 (Biased Locking) —— “贴标签”
-
场景:只有一个线程在访问同步块,没有任何竞争。
-
操作:
- JVM 认为没必要进行昂贵的同步操作。
- 线程第一次访问时,直接通过 CAS 将自己的 线程 ID 记录在对象头的 Mark Word 中。
-
身份标识:Thread ID。
- 潜台词:“这个对象是我的专用工位,上面贴了我的名字。下次我来,看一眼名字对得上,直接坐下。”
-
性能:极高,几乎等同于无锁执行。
第二阶段:轻量级锁 (Lightweight Locking) —— “抢令牌”
-
场景:出现了竞争(线程 A 持有锁时,线程 B 来了),偏向锁失效,升级为轻量级锁。
-
操作:
- 备份:每个竞争线程在自己的**栈帧(Stack Frame)**中创建一个
Lock Record(锁记录)空间,将对象头的 Mark Word 复制一份过来。 - 争抢:线程尝试利用 CAS 指令,将对象头中的指针指向自己栈里的 Lock Record。
- 自旋:抢夺失败的线程不会立即挂起,而是通过自旋(Spinning) (空循环)在用户态等待,赌持有者很快就会释放锁。
- 备份:每个竞争线程在自己的**栈帧(Stack Frame)**中创建一个
-
身份标识:指向线程栈中 Lock Record 的指针。
- 潜台词:“锁的控制权归我(线程)私人所有,凭证就在我的背包里。谁指针对了,谁就是老大。”
-
关键点:全程在用户态完成,避免了内核切换的开销。
第三阶段:重量级锁 (Heavyweight Locking) —— “进大厅”
-
场景:竞争加剧(自旋超过限制或大量线程涌入)。自旋会白白消耗 CPU,必须止损。
-
操作:
- 膨胀:JVM 申请一个 C++ 的
ObjectMonitor对象(管程)。 - 指向:对象头的 Mark Word 指针修改为指向堆内存中的 Monitor 对象。
- 阻塞:抢不到锁的线程不再自旋,而是被封装成 ObjectWaiter 对象,进入 Monitor 的
_EntryList队列,并被操作系统挂起(BLOCKED) 。
- 膨胀:JVM 申请一个 C++ 的
-
身份标识:指向堆中 ObjectMonitor 对象的指针。
- 潜台词:“现在情况太乱了,私人解决不了。所有人都去‘管程办事大厅’(Monitor),那里有专人(OS)负责排队和叫号。”
-
代价:涉及用户态到内核态的上下文切换,成本最高,但保证了 CPU 资源不被空转浪费。
三、 为什么要有“锁升级”?
这是一个从性能到吞吐量的权衡过程。
-
用户态 vs 内核态:
- 轻量级锁的核心优势在于不打扰操作系统。CAS 和自旋都在用户态完成,速度极快。
- 重量级锁需要挂起线程,这需要操作系统介入(上下文切换),保存寄存器、刷新缓存,开销很大。
-
止损策略:
- 如果锁竞争不激烈且持有时间短,自旋(轻量级锁)是最高效的,因为它避免了线程切换的开销。
- 如果竞争激烈或持有时间长,自旋就会导致 CPU 空转(由 100% 占用但无产出)。此时,必须升级为重量级锁,让线程“睡觉”,把 CPU 让给其他有意义的任务。
四、 总结
synchronized 的锁升级机制,本质上是 JVM 对线程并发场景的一种自适应算法:
- 偏向锁:优化“单人独占”场景,只记录身份 ID。
- 轻量级锁:优化“短期交替”场景,通过指针指向线程栈,利用 CAS 和自旋避免线程挂起。
- 重量级锁:应对“高并发竞争”场景,通过指针指向 Monitor,利用操作系统的队列机制保证调度的公平与稳定。
理解了这一点,你就理解了为什么 Java 可以在不改变代码逻辑的情况下,通过底层的动态演进,实现从“轻量”到“重量”的平滑过渡。