Java 中的 synchronized 关键字是实现线程安全与同步的核心手段,其底层依赖于 JVM 提供的 Monitor(管程 / 监视器锁) 机制。
Monitor 不仅在语义层保证了并发访问的互斥性与可见性,更在底层的 Java 内存模型(JMM)与现代 CPU 硬件指令集上建立了严密的内存屏障与 Happens-Before 关联。
JMM 内存模型与 Happens-Before 规则
Java 内存模型(JMM)通过 Happens-Before 原则定义了多线程环境下的内存可见性与有序性保障。Monitor 机制是 JMM Happens-Before 规则的核心载体之一。
监视器锁规则 (Monitor Lock Rule)
JMM 明确规定:对一个监视器锁的解锁(Unlock)Happens-Before 于后续对同一个监视器锁的加锁(Lock)。
内存屏障与硬件级指令映射 (Memory Barriers)
为了实现 Monitor 锁的 Happens-Before 语义,JVM 在编译与字节码执行时会在 Monitor 的临界区边界插入硬件级内存屏障 (Memory Barrier):
- 获取锁 (
monitorenter):- JVM 会插入
LoadLoad+LoadStore屏障。 - 功效:清空线程本地工作内存(L1/L2 Cache),强制后续所有共享变量的读写操作必须直接从主内存(Main Memory)重新加载,保证读到的总是最新值。
- JVM 会插入
- 释放锁 (
monitorexit):- JVM 会插入
StoreStore+StoreLoad屏障。 - 功效:强制将线程工作内存中对共享变量的所有修改(Write Buffer / Store Buffer 积压刷新)立即刷回主内存,确保这些修改对后续抢锁线程绝对可见。
- JVM 会插入
Monitor 内部底层结构与锁膨胀机制
在 HotSpot JVM 中,每个对象都可以充当 Monitor 锁。对象的Mark Word(对象头) 在不同锁状态下指向不同的物理结构。
HotSpot ObjectMonitor C++ 结构解析
当锁膨胀为重量级锁(Heavyweight Lock)时,Mark Word 会指针关联到一个 C++ ObjectMonitor 结构体:
// HotSpot 源码 hotspot/src/share/vm/runtime/objectMonitor.hpp
ObjectMonitor() {
_header = NULL;
_count = 0; // 重入计数器
_waiters = 0;
_recursions = 0; // 锁重入次数
_object = NULL; // 指向被锁的对象
_owner = NULL; // 指向当前持有 Monitor 的 Thread 指针
_WaitSet = NULL; // 处于 wait() 状态的线程单向链表
_WaitSetLock = 0;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL; // 竞争锁失败的单向 LIFO 栈 (_cxq)
FreeNext = NULL;
_EntryList = NULL; // 准备竞争锁的双向等待队列
_spinFreq = 0;
_spinClock = 0;
OwnerIsThread = 0;
}
Java Monitor 如何支持锁队列与 Condition 队列?
与 Java AQS(依赖 state + 双向 CLH 队列 + 单向 Condition 队列)的架构思想高度契合,JVM 底层的 ObjectMonitor(C++ 实现) 本质上也维护了两类物理队列来实现锁竞争(锁队列)与线程协同(Condition 队列):
1. 核心封装节点:ObjectWaiter
在 C++ 内部,JVM 并不是直接把原生 Thread 指针放入队列,而是把线程封装为一个 ObjectWaiter 结构体(类似于 AQS 的 Node):
class ObjectWaiter : public StackObj {
public:
enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ };
ObjectWaiter* _next;
ObjectWaiter* _prev;
Thread* _thread;
volatile TStates TState;
bool _active;
// ...
};
2. 锁队列机制:_cxq 与 _EntryList
Java Monitor 的锁队列由 _cxq 和 _EntryList 配合组成:
_cxq(Contention Queue,单向 LIFO 栈):- 入队:当线程执行
monitorenter抢锁失败时,JVM 会创建一个ObjectWaiter,通过 CAS 争抢将节点压入_cxq的栈头。由于使用了 CAS 栈顶更新,多线程并发入队无需加锁。
- 入队:当线程执行
_EntryList(准备获锁的 FIFO 队列):- 转移与唤醒:当持有锁的线程执行
monitorexit释放锁时,如果_EntryList为空,持锁线程会将_cxq中的所有节点一次性转移到_EntryList中,然后再唤醒_EntryList头的线程去抢锁。 - 分流设计的意义:将高并发 CAS 入队的
_cxq与顺序唤醒出队的_EntryList解耦,极大地降低了锁释放时的队列头尾竞争。
- 转移与唤醒:当持有锁的线程执行
3. Condition 队列机制:_WaitSet
_WaitSet 是 JVM 用来实现 Object.wait() 和 Object.notify() 的物理条件队列:
Object.wait()过程(挂入 Condition 队列):- 线程持锁期间调用
obj.wait()。 - 线程创建或复用
ObjectWaiter节点,将其标记为TS_WAIT状态。 - 将节点插入到
_WaitSet链表的尾部。 - 完全释放当前持有 Monitor 锁的所有权(
_owner = NULL,重入计数器_recursions清零)。 - 唤醒
_EntryList或_cxq中的线程来接管锁。 - 当前线程调用
os::PlatformEvent::park()挂起休眠。
- 线程持锁期间调用
Object.notify()过程(节点转移到锁队列):- 持锁线程调用
obj.notify()。 - JVM 遍历
_WaitSet,弹出队首的第一个有效ObjectWaiter节点。 - 节点转移(Transfer):JVM 根据策略(由
Knob_Knock配置决定),将该节点从_WaitSet移出,并重新插入到_EntryList或_cxq锁队列中。 - 节点的状态从
TS_WAIT变为TS_ENTER或TS_CXQ。 - 被唤醒的线程并不会立即执行,而是必须等待当前线程释放锁后,在锁队列中重新竞争到锁,才能从
wait()代码处返回!
- 持锁线程调用
4. Monitor 队列 vs AQS 队列对比
| 对比维度 | JVM Monitor 底层实现 | Java AQS 框架实现 |
|---|---|---|
| 锁队列物理结构 | _cxq (单向 LIFO 栈) + _EntryList (双向队列) | 双向 CLH 队列 (head / tail) |
| Condition 队列 | _WaitSet (单向/双向链表) | 单向 ConditionObject 链表 |
| Condition 数量 | 每个 Monitor 对象仅支持 1 个 _WaitSet | 1 个 Lock 支持创建多个独立 Condition 队列 |
| 节点转移机制 | notify() 时将节点由 _WaitSet 移入 _EntryList / _cxq | signal() 时将节点由 Condition 队列移入双向 CLH 队列尾部 |
| 抢锁自旋与挂起 | CAS 压入 _cxq $\to$ os::PlatformEvent::park() | CAS 压入 CLH 尾部 $\to$ LockSupport.park() |
锁膨胀与升级机制 (Lock Escalation)
为了在低竞争场景下避免重量级锁昂贵的内核态切换(pthread_mutex / futex),HotSpot JVM 设计了锁膨胀升级机制:
- 偏向锁(Biased Locking):
- 假设“锁总是由同一个线程多次获得”。
- 首次获取锁时,在 Mark Word 中记录 ThreadID;后续该线程再次进入时,仅需比对 ThreadID,完全无需 CAS 操作。
- 轻量级锁(Lightweight Locking):
- 当有第二个线程尝试获取锁时,偏向锁撤销并升级为轻量级锁。
- 线程在各自的线程栈帧中创建
Lock Record,通过 CAS 将 Mark Word 指向Lock Record。抢锁失败时采用适应性自旋等待,避免让出 CPU。
- 重量级锁(Heavyweight Locking):
- 当自旋达到一定次数,或竞争过于激烈时,轻量级锁膨胀为重量级锁。
- JVM 申请分配底层 C++
ObjectMonitor,争抢失败的线程由内核态调用pthread_mutex/futex挂起(park),进入阻塞休眠状态。
Monitor 在 Java 中的主要应用场景
1. 语言级同步块与同步方法 (synchronized)
- 同步代码块:编译器生成
monitorenter和monitorexit字节码指令。为了保证异常情况下锁依然被释放,编译器会自动生成隐式的try-finally异常处理块,在异常路径上追加monitorexit。 - 同步方法:静态或实例方法修饰
synchronized时,字节码flags中会标注ACC_SYNCHRONIZED标志位,JVM 在调用方法时自动隐式执行monitorenter与monitorexit。
2. 传统线程间协作 (Object.wait() / Object.notify())
- 配合要求:调用
wait()、notify()或notifyAll()前,线程必须首先持有该对象的 Monitor 锁(即位于synchronized块内),否则抛出IllegalMonitorStateException。 - 协作原理:
wait():线程释放当前 Monitor 锁的所有权(_owner归零,重入计数器_recursions暂存),线程被封装放入_WaitSet并休眠。notify():从_WaitSet中挑选一个等待节点,将其转移到_EntryList或_cxq中,使其重新获得竞争 Monitor 锁的资格。
3. Java 基础并发容器实现
Vector/Hashtable:所有核心读写方法均使用synchronized修饰,依赖 Monitor 保障线程安全。Collections.synchronizedList()/synchronizedMap():包装器模式,内部维护一个final Object mutex对象,所有读写方法均在synchronized(mutex)块中完成。- Early JDK
ConcurrentHashMap:JDK 1.7 之前的 Segment 锁,以及 JDK 1.8 中在哈希桶首节点上的synchronized细粒度锁定。
Monitor 锁 vs AQS (ReentrantLock) 对比
| 对比维度 | JVM Monitor (synchronized) | Java AQS (ReentrantLock) |
|---|---|---|
| 实现层级 | JVM C++ 引擎与字节码 (monitorenter/exit) | Java 语言层 SDK (JUC 包 Unsafe / VarHandle CAS) |
| 底层锁膨胀 | 支持 (无锁 $\to$ 偏向锁 $\to$ 轻量级锁 $\to$ 重量级锁) | 不支持 (依赖 state CAS 结合 LockSupport.park) |
| 灵活度与控制力 | 自动加锁/释放,隐式异常处理,灵活性受限 | 手动 lock() / unlock(),支持 tryLock() 超时与中断 |
| 条件等待队列 | 每个 Monitor 锁仅对应 1 个等待池 (_WaitSet) | 1 个 Lock 可创建 多个独立的 Condition 队列 |
| 公平性选择 | 仅支持非公平锁 | 原生支持公平锁与非公平锁构造选择 |
| Happens-Before | 自动建立 monitorexit $\to$ monitorenter 规则 | 依靠 volatile state 变量读写建立 HB 规则 |