synchronized、监视器锁、Mark Word、轻量级锁与重量级锁:一次系统梳理

本文整理自一次关于 Java synchronized 锁机制的问答。重点讨论:

  • synchronized 为什么说是“监视器锁”?
  • 为什么又说只有重量级锁才真正指向 ObjectMonitor
  • Mark Word、owner、等待队列、计数器分别属于什么层次?
  • CAS 是不是只有轻量级锁才有用?
  • 偏向锁的废除是不是因为虚拟线程?

1. 偏向锁的废除是不是因为虚拟线程?

问题

偏向锁的废除是不是因为虚拟线程?

回答

不是。偏向锁的废除不是因为虚拟线程,至少官方动机不是这个。

从时间线上看也能说明问题:

  • 偏向锁在 JDK 15 通过 JEP 374 被默认禁用并标记废弃。
  • JEP 374 的核心目标,是评估是否还需要继续支持这种维护成本较高的老式同步优化。
  • JEP 374 提到的原因主要是:现代应用的同步模式已经变化,偏向锁带来的收益变小,而 JVM 内部实现复杂度和维护成本仍然较高。
  • 偏向锁相关代码后来在 JDK 18 被进一步废弃或移除。
  • 虚拟线程最早作为预览特性进入 JDK,是 JDK 19 的 JEP 425。

因此,从主线时间上看,偏向锁默认关闭和废弃,早于虚拟线程正式进入 JDK。

更准确的说法是:

偏向锁被废除,主要因为它是针对早期 Java 代码风格的优化。当时大量代码使用 VectorHashtableStringBuffer 这类内部带 synchronized 的类,而且很多锁实际上只被单线程反复进入。

在这种场景下,偏向锁可以避免无竞争锁的 CAS 成本。

但后来的 Java 应用更多使用:

  • java.util.concurrent 包中的并发工具;
  • 无锁结构;
  • 线程池;
  • 框架级并发控制;
  • 更细粒度的并发模型。

这导致 synchronized 的使用模式发生变化。偏向锁的收益不再普遍,而它给 JVM 带来的复杂度仍然很高。

偏向锁会影响很多底层模块,例如:

  • 对象头 Mark Word 的布局;
  • 锁撤销;
  • safepoint;
  • GC;
  • JIT 编译;
  • 监控和诊断工具。

所以,偏向锁被废除的核心原因是:收益下降,维护成本高,同步模式变化

虚拟线程和这件事有间接关系,但不是直接原因。

虚拟线程确实对 synchronized 提出了新的压力。例如,虚拟线程在进入 synchronized 或执行某些 native / foreign 调用时,可能出现 carrier thread 被 pin 住的问题,从而影响调度弹性。

但这属于 Loom / 虚拟线程时代对锁实现继续演进的背景压力,不是 JEP 374 废除偏向锁的官方因果链。

小结

不是因为虚拟线程废除了偏向锁。偏向锁是先因为收益下降、维护成本高、同步模式变化而被废弃;虚拟线程只是后来让 JVM 锁实现“更简单、更可维护”这个方向显得更合理。


2. synchronized 是监视器锁,但为什么又说只有重量级锁才指向监视器?

问题

我对 synchronized 锁有点混乱。

它是一个监视器锁,监视器有 owner、等待队列、计数器等结构。

但是也有一种说法:对于一个 synchronized 锁,只有最后一级的重量级锁,在对象 Mark Word 中才会指向监视器锁。

那么介绍底层的时候,为什么还要介绍它是一个监视器锁?

还有,CAS 是不是只有轻量级锁开始才有用?

回答

这个问题的关键在于:Java 语义层面HotSpot 实现层面被混在一起讲了。


3. Java 语义层面:synchronized 就是监视器锁

在 Java 语义中,每个对象都可以作为一个 monitor。

代码:

synchronized (obj) {
    // 临界区
}

语义上就是:

进入 obj 对应的 monitor
执行同步代码
退出 obj 对应的 monitor

也就是说,synchronized(obj) 的语义就是“进入 obj 的监视器”。退出同步块时,就是“退出 obj 的监视器”。

wait()notify()notifyAll() 也和 monitor 密切相关。

例如:

synchronized (obj) {
    obj.wait();
}

调用 wait() 之前,当前线程必须已经持有 obj 的 monitor,否则会抛出 IllegalMonitorStateException

所以教材、Java 规范、并发书里会说:

synchronized 是 monitor lock。

这句话说的是语义模型

在语义层面,它始终成立。


4. HotSpot 实现层面:不一定一开始就创建 ObjectMonitor

在 HotSpot JVM 的具体实现中,为了优化性能,synchronized 不一定一开始就创建或使用完整的重量级 ObjectMonitor

早期 HotSpot 常见的锁状态大致包括:

无锁
  ↓
偏向锁
  ↓
轻量级锁
  ↓
重量级锁

对象头中的 Mark Word 会记录和锁相关的信息。

不同锁状态下,Mark Word 的内容可能不同。

例如,在传统 HotSpot 实现中:

  • 无锁状态下,Mark Word 主要存储对象 hash、GC 年龄、锁标志位等信息;
  • 偏向锁状态下,Mark Word 可能记录偏向线程 ID、epoch、锁标志位等信息;
  • 轻量级锁状态下,Mark Word 可能指向当前线程栈帧里的 Lock Record;
  • 重量级锁状态下,Mark Word 可能指向 ObjectMonitor

因此,下面两句话并不矛盾:

synchronized 是监视器锁。

和:

只有重量级锁时,对象 Mark Word 才指向 Monitor / ObjectMonitor。

前者说的是 Java 语义。

后者说的是 HotSpot 的某些版本中的具体实现。

更准确地说:

所有 synchronized 在语义上都是 monitor,但不是所有 synchronized 在实现上都会立刻膨胀成 ObjectMonitor


5. 为什么底层介绍 synchronized 时还要讲 ObjectMonitor?

因为 ObjectMonitorsynchronized 重量级路径的核心结构。

当锁发生竞争、需要阻塞线程、需要唤醒线程,或者涉及 wait() / notify() / notifyAll() 这些复杂语义时,轻量级路径已经不够用了。

这时锁会膨胀为重量级锁,底层就会使用 ObjectMonitor

典型的 ObjectMonitor 中会包含类似这些字段或结构:

owner       当前持有锁的线程
recursions  重入次数
EntryList   竞争锁失败后等待进入同步区的线程集合
cxq         竞争队列,具体实现和版本有关
WaitSet     调用 wait() 后进入等待状态的线程集合

所以,介绍 synchronized 底层时讲 ObjectMonitor 并不是错的。

但要加一个限定:

ObjectMonitor 主要描述的是锁膨胀后的重量级实现,不代表每次进入 synchronized 都一定会直接使用完整的 ObjectMonitor

最容易误解的一句话是:

每个 Java 对象都有一个 Monitor。

这句话如果按语义理解,可以接受:每个 Java 对象都可以作为 monitor 使用。

但如果按 HotSpot 实现理解,就不够严谨。

更准确的说法应该是:

每个 Java 对象都可以关联一个 monitor;但 HotSpot 不一定一开始就为每个对象分配 ObjectMonitor,只有需要时才会膨胀或关联。


6. Mark Word 指向 Monitor 只发生在重量级锁吗?

在传统 HotSpot 锁实现中,可以这样理解:

  • 轻量级锁阶段,Mark Word 可能指向线程栈上的 Lock Record;
  • 重量级锁阶段,Mark Word 可能指向堆或 JVM 内部结构中的 ObjectMonitor

所以“Mark Word 指向 Monitor”通常是在讲重量级锁。

但是需要注意:新版 HotSpot 的锁实现已经发生变化。

在新的 lightweight locking 设计中,轻量级锁不再像旧实现那样把栈上 Lock Record 的地址塞进对象头,而是尽量让 Mark Word 只保留少量锁状态位。

也就是说,网上很多图示:

Mark Word -> Lock Record
Mark Word -> ObjectMonitor

主要是在描述传统 HotSpot 锁实现,尤其是 JDK 6 到 JDK 17 附近常见的模型。

它有助于理解历史上的锁升级机制,但不能无条件当作所有 JDK 版本的永恒规则。


7. CAS 是不是只有轻量级锁开始才有用?

不是。

更准确地说:

CAS 是 synchronized 快速路径里的核心手段之一,但不同锁阶段使用方式不同。

7.1 偏向锁中的 CAS

偏向锁的目标,是在单线程反复进入同一把锁时,尽量避免 CAS。

如果对象已经偏向当前线程,那么当前线程再次进入同步块时,通常只需要检查 Mark Word,不需要 CAS。

这是偏向锁的核心收益。

但是偏向锁不是完全不用 CAS。

例如:

  • 对象还没有偏向任何线程时,第一次建立偏向可能需要 CAS;
  • 发现对象偏向了别的线程时,可能需要撤销偏向、重偏向或升级;
  • 偏向锁撤销过程中也可能涉及原子操作和 safepoint。

所以:

偏向锁的理想快路径是不使用 CAS,但偏向锁机制整体并不是完全不使用 CAS。

7.2 轻量级锁中的 CAS

轻量级锁阶段,CAS 非常关键。

在传统 HotSpot 实现中,线程进入同步块时,会在线程栈中创建 Lock Record,然后尝试用 CAS 把对象 Mark Word 替换成指向 Lock Record 的指针。

大致流程可以理解成:

1. 线程进入 synchronized。
2. 在线程栈帧中创建 Lock Record。
3. 将对象当前 Mark Word 拷贝到 Lock Record 中。
4. 使用 CAS 尝试把对象 Mark Word 替换为指向 Lock Record 的指针。
5. CAS 成功,表示轻量级加锁成功。
6. CAS 失败,说明可能有竞争、重入或需要膨胀。

因此,轻量级锁是最典型的 CAS 使用场景。

7.3 重量级锁中的 CAS

重量级锁中仍然可能使用 CAS。

例如:

  • 尝试把 ObjectMonitor 的 owner 从 null CAS 成当前线程;
  • 竞争队列入队、出队时使用原子操作;
  • 锁释放和状态转换时使用原子操作;
  • 某些自旋优化路径中使用 CAS。

只不过,重量级锁不再只是 CAS。

它还涉及:

  • 线程阻塞;
  • 线程唤醒;
  • park / unpark;
  • 操作系统调度;
  • JVM monitor slow path。

所以不能说 CAS 只有轻量级锁才有用。

更准确的总结是:

偏向锁:目标是尽量避免 CAS,但建立偏向或撤销偏向时仍可能用 CAS。
轻量级锁:CAS 是核心获取手段。
重量级锁:仍可能用 CAS,但还会涉及阻塞、唤醒和调度。

8. synchronized 的字节码层面

问题

synchronized 锁是一个监视器锁。对于锁的进入和释放,本质是执行字节码命令。

进入时执行计数器 +1,同时 CAS 操作修改 owner 为当前线程。

其他线程访问时会进入监视器里的等待队列。

代码离开同步区时执行计数器 -1,计数器为 0 时释放该锁,同时唤醒等待队列里的线程,它们通过竞争 CAS 获取该锁。

但是这仅限于重量级锁,对吗?

回答

这个描述接近重量级 ObjectMonitor 的工作方式,但有几个细节需要修正。


9. synchronized 进入和退出:不一定都是显式 monitorenter / monitorexit

对于同步代码块:

public void test(Object obj) {
    synchronized (obj) {
        System.out.println("hello");
    }
}

编译后的字节码中通常会出现:

monitorenter
monitorexit

monitorenter 表示进入对象 monitor。

monitorexit 表示退出对象 monitor。

而且为了保证异常情况下也能释放锁,编译器通常会生成多个 monitorexit,配合异常表保证同步块正常退出或异常退出时都能释放锁。

但是对于同步方法:

public synchronized void test() {
    System.out.println("hello");
}

它通常不会在方法体内显式生成 monitorenter / monitorexit

同步方法是通过方法访问标志 ACC_SYNCHRONIZED 表示的。

JVM 在调用该方法和方法返回时,会隐式完成 monitor 的进入和退出。

所以不能简单说:

所有 synchronized 都是执行 monitorenter / monitorexit 字节码。

更准确应该说:

同步代码块使用 monitorenter / monitorexit;同步方法通过 ACC_SYNCHRONIZED 方法标志,由 JVM 隐式加锁和解锁。


10. owner 和递归计数不是所有锁阶段都有的完整结构

重量级 ObjectMonitor 里确实有 owner 和递归计数。

但是“进入时计数器 +1,同时 CAS 修改 owner 为当前线程”这个说法偏重量级锁,而且还需要进一步修正。

在重量级 monitor 中,可以这样理解:

第一次获得锁:
    owner 设置为当前线程。

同一线程再次进入同一把锁:
    发生重入,递归计数增加。

退出同步块:
    如果存在递归计数,则递归计数减少。

最后一次退出:
    清空 owner,真正释放锁。

也就是说,递归计数主要表示重入层数,而不是简单地等价于“每次进入都 +1,每次退出都 -1”。

不同实现中,第一次进入时递归计数如何表示会有细节差异。

更安全的理解是:

owner 表示当前持锁线程,recursions 表示重入相关状态。锁只有在最后一层退出时才真正释放。


11. 竞争队列和 WaitSet 不是同一个东西

“其他线程访问时会进入监视器里的等待队列”这句话容易混淆两个概念。

重量级 ObjectMonitor 中通常有两类队列或集合:

EntryList / cxq:
    竞争 synchronized 锁失败的线程等待重新竞争锁。

WaitSet:
    调用 obj.wait() 后进入等待状态的线程集合。

这两个不是一回事。

例如:

synchronized (obj) {
    // 临界区
}

如果线程 A 持有 obj 的锁,线程 B 也想进入这个同步块,但竞争失败,那么线程 B 等待的是进入 monitor 的机会。

它可能进入的是 EntryList / cxq 这类竞争队列,而不是 WaitSet。

而下面这种情况才会进入 WaitSet:

synchronized (obj) {
    obj.wait();
}

调用 wait() 后,当前线程会:

1. 释放当前持有的 obj monitor;
2. 进入 obj monitor 的 WaitSet;
3. 等待其他线程执行 notify 或 notifyAll;
4. 被唤醒后重新参与锁竞争;
5. 重新获得锁之后,才从 wait() 返回继续执行。

因此:

  • 竞争 synchronized 失败的线程,不是进入 WaitSet;
  • 调用 wait() 的线程,才进入 WaitSet;
  • notify() / notifyAll() 唤醒的是 WaitSet 中的线程;
  • 被 notify 的线程不会立刻执行,它仍然需要重新竞争锁。

12. 释放锁时是否唤醒等待线程?

在重量级锁中,线程退出同步区时,如果这是最后一次退出,monitor 会释放 owner。

之后 JVM 可能会唤醒或放行某个阻塞线程,让它重新竞争锁。

这个过程可能涉及:

  • CAS;
  • 自旋;
  • park / unpark;
  • 竞争队列转移;
  • JVM slow path;
  • 操作系统线程调度。

所以“释放锁后唤醒等待线程,让它们通过竞争 CAS 获取锁”这个方向大体可以帮助理解,但不要把它理解成一个非常固定的、所有版本都一致的流程。

更准确地说:

重量级锁释放时会清空 owner,并可能唤醒或放行竞争线程;被唤醒线程需要重新参与锁竞争,具体实现可能包含 CAS、自旋、阻塞和调度。


13. 轻量级锁阶段没有完整的 ObjectMonitor 队列

轻量级锁的目标是:在没有实际竞争,或者竞争很轻微时,避免膨胀成重量级 monitor。

因此,在轻量级锁阶段通常不会完整使用:

  • ObjectMonitor.owner
  • ObjectMonitor.EntryList
  • ObjectMonitor.WaitSet
  • 操作系统阻塞唤醒;
  • 完整 monitor slow path。

传统实现中,轻量级锁主要依赖:

  • 对象头 Mark Word;
  • 线程栈中的 Lock Record;
  • CAS;
  • 必要时自旋;
  • 失败后膨胀为重量级锁。

所以你说的这套流程:

进入时 CAS 修改 owner 为当前线程;
竞争失败进入 monitor 等待队列;
退出时计数器 -1;
计数器为 0 时释放锁;
唤醒等待队列线程重新竞争。

更适合描述重量级 ObjectMonitor,不适合直接套到轻量级锁上。


14. 一个更准确的 synchronized 总流程

可以把 synchronized 分成三层理解。

14.1 第一层:Java 语义层

synchronized 的语义是进入和退出对象 monitor。

不管底层有没有真的膨胀成 ObjectMonitor,Java 语义上都可以说它是监视器锁。

14.2 第二层:HotSpot 快速路径

无竞争或低竞争时,JVM 尽量使用轻量方式完成加锁。

可能涉及:

  • Mark Word;
  • CAS;
  • Lock Record;
  • 轻量级锁状态;
  • JIT 生成的 fast path。

这个阶段不一定创建完整 ObjectMonitor

14.3 第三层:HotSpot 慢速路径 / 重量级锁

竞争激烈、需要阻塞、需要 wait/notify,或者轻量级路径失败时,锁膨胀成重量级 ObjectMonitor。

此时可能涉及:

  • owner;
  • recursions;
  • EntryList;
  • cxq;
  • WaitSet;
  • park / unpark;
  • 线程阻塞和唤醒。

15. 修正后的完整表述

原先的表述:

synchronized 锁是一个监视器锁。对于锁的进入和释放,本质是执行字节码命令。当进入时执行计数器 +1,同时 CAS 操作修改 owner 为当前线程。其他线程访问时会进入监视器里的等待队列。当代码离开同步区会执行计数器 -1,计数器为 0 时释放该锁,同时唤醒等待队列里的线程,它们通过竞争 CAS 获取该锁。但是这仅限于重量级锁。

可以修正为:

synchronized 在 Java 语义上是对象监视器锁。同步代码块通过 monitorenter / monitorexit 表达进入和退出 monitor;同步方法则通过 ACC_SYNCHRONIZED 方法标志,由 JVM 隐式完成加锁和解锁。

在 HotSpot 实现中,synchronized 不一定一开始就使用完整的 ObjectMonitor。无竞争或低竞争时,JVM 会尽量走轻量级快速路径,例如通过 Mark Word、线程栈 Lock Record、CAS 等机制完成加锁。只有当竞争加剧、需要阻塞线程、调用 wait(),或者轻量级路径无法继续时,锁才会膨胀为重量级 ObjectMonitor

膨胀为重量级锁后,ObjectMonitor 会维护 owner、重入计数、竞争队列以及 WaitSet 等结构。线程第一次获得 monitor 时会成为 owner;同一线程重入时更新递归计数;竞争失败的线程进入 EntryList / cxq 等竞争队列;调用 wait() 的线程释放锁并进入 WaitSet。退出同步区时,JVM 会处理递归计数,最后一次退出时释放 owner,并可能唤醒或放行其他竞争线程重新参与锁竞争。

因此,owner、递归计数、EntryList、WaitSet、阻塞唤醒这些完整 monitor 结构,主要描述的是重量级 ObjectMonitor 路径,不能直接套到所有 synchronized 场景上。


16. 最终总结

可以用一句话概括:

synchronized 在语义上始终是 monitor lock;但在 HotSpot 实现上,它会尽量先走轻量级快速路径,只有在必要时才膨胀为重量级 ObjectMonitor

再进一步拆开:

语义层:
    synchronized = 进入 / 退出对象 monitor。

字节码层:
    同步代码块 = monitorenter / monitorexit。
    同步方法 = ACC_SYNCHRONIZED。

轻量级实现层:
    主要依靠 Mark Word、Lock Record、CAS、fast path。
    不一定存在完整 ObjectMonitor。

重量级实现层:
    由 ObjectMonitor 承载。
    包含 owner、recursions、EntryList、WaitSet 等结构。
    支持阻塞、唤醒、wait、notify 等复杂语义。

CAS:
    偏向锁阶段可能用,但目标是尽量避免 CAS。
    轻量级锁阶段是核心手段。
    重量级锁阶段仍可能使用,但还会结合阻塞和调度。

所以最关键的认知是:

不要把 Java 语义上的 monitor 和 HotSpot 实现中的 ObjectMonitor 完全画等号。

它们有关联,但不是一个层次的概念。


17. 参考关键词

如果后续继续深入,可以围绕以下关键词查资料:

  • Java monitor
  • monitorenter
  • monitorexit
  • ACC_SYNCHRONIZED
  • HotSpot Mark Word
  • Lightweight Locking
  • ObjectMonitor
  • EntryList
  • WaitSet
  • wait() / notify() / notifyAll()
  • biased locking
  • JEP 374
  • virtual threads
  • carrier thread pinning
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐