Synchronized 底层揭秘:从对象头到操作系统管程的演进之路

0 阅读5分钟

在 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 在字节码层面对应的是 monitorentermonitorexit 指令。这两个指令背后的实现依赖于操作系统的 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 来了),偏向锁失效,升级为轻量级锁。

  • 操作

    1. 备份:每个竞争线程在自己的**栈帧(Stack Frame)**中创建一个 Lock Record(锁记录)空间,将对象头的 Mark Word 复制一份过来。
    2. 争抢:线程尝试利用 CAS 指令,将对象头中的指针指向自己栈里的 Lock Record
    3. 自旋:抢夺失败的线程不会立即挂起,而是通过自旋(Spinning) (空循环)在用户态等待,赌持有者很快就会释放锁。
  • 身份标识指向线程栈中 Lock Record 的指针

    • 潜台词:“锁的控制权归我(线程)私人所有,凭证就在我的背包里。谁指针对了,谁就是老大。”
  • 关键点:全程在用户态完成,避免了内核切换的开销。

第三阶段:重量级锁 (Heavyweight Locking) —— “进大厅”

  • 场景:竞争加剧(自旋超过限制或大量线程涌入)。自旋会白白消耗 CPU,必须止损。

  • 操作

    1. 膨胀:JVM 申请一个 C++ 的 ObjectMonitor 对象(管程)。
    2. 指向:对象头的 Mark Word 指针修改为指向堆内存中的 Monitor 对象
    3. 阻塞:抢不到锁的线程不再自旋,而是被封装成 ObjectWaiter 对象,进入 Monitor 的 _EntryList 队列,并被操作系统挂起(BLOCKED)
  • 身份标识指向堆中 ObjectMonitor 对象的指针

    • 潜台词:“现在情况太乱了,私人解决不了。所有人都去‘管程办事大厅’(Monitor),那里有专人(OS)负责排队和叫号。”
  • 代价:涉及用户态到内核态的上下文切换,成本最高,但保证了 CPU 资源不被空转浪费。

三、 为什么要有“锁升级”?

这是一个从性能吞吐量的权衡过程。

  1. 用户态 vs 内核态

    • 轻量级锁的核心优势在于不打扰操作系统。CAS 和自旋都在用户态完成,速度极快。
    • 重量级锁需要挂起线程,这需要操作系统介入(上下文切换),保存寄存器、刷新缓存,开销很大。
  2. 止损策略

    • 如果锁竞争不激烈且持有时间短,自旋(轻量级锁)是最高效的,因为它避免了线程切换的开销。
    • 如果竞争激烈或持有时间长,自旋就会导致 CPU 空转(由 100% 占用但无产出)。此时,必须升级为重量级锁,让线程“睡觉”,把 CPU 让给其他有意义的任务。

四、 总结

synchronized 的锁升级机制,本质上是 JVM 对线程并发场景的一种自适应算法

  • 偏向锁:优化“单人独占”场景,只记录身份 ID。
  • 轻量级锁:优化“短期交替”场景,通过指针指向线程栈,利用 CAS 和自旋避免线程挂起。
  • 重量级锁:应对“高并发竞争”场景,通过指针指向 Monitor,利用操作系统的队列机制保证调度的公平与稳定。

理解了这一点,你就理解了为什么 Java 可以在不改变代码逻辑的情况下,通过底层的动态演进,实现从“轻量”到“重量”的平滑过渡。