AQS与JVM的ObjectMonitor机制对比分析
- 前言
- AQS与JVM的ObjectMonitor机制对比分析
-
- [一、 核心架构对比概览](#一、 核心架构对比概览)
- [二、 队列结构深度对比](#二、 队列结构深度对比)
-
- [1. `ObjectMonitor` 的三队列架构](#1.
ObjectMonitor的三队列架构) -
- [C++ 源码数据结构 (`objectMonitor.hpp`):](#C++ 源码数据结构 (
objectMonitor.hpp):)
- [C++ 源码数据结构 (`objectMonitor.hpp`):](#C++ 源码数据结构 (
- [2. AQS 的双队列架构(CLH 变体 + ConditionObject)](#2. AQS 的双队列架构(CLH 变体 + ConditionObject))
-
- [Java 源码数据结构 (`AbstractQueuedSynchronizer.java`):](#Java 源码数据结构 (
AbstractQueuedSynchronizer.java):)
- [Java 源码数据结构 (`AbstractQueuedSynchronizer.java`):](#Java 源码数据结构 (
- [1. `ObjectMonitor` 的三队列架构](#1.
- [三、 CAS 抢锁机制与自旋逻辑对比](#三、 CAS 抢锁机制与自旋逻辑对比)
-
- [1. `ObjectMonitor` 的 CAS 与自适应自旋 (Adaptive Spinning)](#1.
ObjectMonitor的 CAS 与自适应自旋 (Adaptive Spinning)) -
- [自适应自旋 (Adaptive Spinning) 的底层机理:](#自适应自旋 (Adaptive Spinning) 的底层机理:)
- [2. AQS 的 CAS 与抢锁逻辑](#2. AQS 的 CAS 与抢锁逻辑)
-
- [AQS 抢锁特点:](#AQS 抢锁特点:)
- [1. `ObjectMonitor` 的 CAS 与自适应自旋 (Adaptive Spinning)](#1.
- [四、 唤醒策略 (Unpark) 与线程继承机制对比](#四、 唤醒策略 (Unpark) 与线程继承机制对比)
-
- [1. `ObjectMonitor` 的 `exit()` 与 `_succ` 继承人机制](#1.
ObjectMonitor的exit()与_succ继承人机制) -
- [`_succ` (Successor) 机制设计意图:](#
_succ(Successor) 机制设计意图:)
- [`_succ` (Successor) 机制设计意图:](#
- [2. AQS 的 `release()` 与 Tail-to-Head 逆向扫描](#2. AQS 的
release()与 Tail-to-Head 逆向扫描) -
- [AQS 为什么必须采用"从尾到头 (Tail-to-Head)"逆向扫描?](#AQS 为什么必须采用“从尾到头 (Tail-to-Head)”逆向扫描?)
- [1. `ObjectMonitor` 的 `exit()` 与 `_succ` 继承人机制](#1.
- [五、 响应中断与并发取消 (Cancellation) 机制](#五、 响应中断与并发取消 (Cancellation) 机制)
-
- [1. AQS 的严格中断与取消状态处理](#1. AQS 的严格中断与取消状态处理)
- [2. `ObjectMonitor` 的撤销与中断处理](#2.
ObjectMonitor的撤销与中断处理)
- [六、 终极总结与选择权衡](#六、 终极总结与选择权衡)
- [七、 全维度对比总结与选型启发](#七、 全维度对比总结与选型启发)
-
- [1. 深度对比矩阵](#1. 深度对比矩阵)
- [2. 总结](#2. 总结)
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
AQS与JVM的ObjectMonitor机制对比分析
JVM 底层的 ObjectMonitor (C++ 实现,synchronized 的基石)与 Java 层的 AbstractQueuedSynchronizer (AQS) (Java 语言层面 ReentrantLock、Semaphore 等并发工具的基石)均采用了 Mesa 管程模型 的思想。然而,由于二者所在的运行层级不同(JVM C++ 内部 vs. Java 虚拟机字节码/类库层),它们在队列结构 、唤醒策略 与CAS 抢锁机制上有着非常深刻的架构差异。
一、 核心架构对比概览
| 维度 | JVM 底层 ObjectMonitor (C++) |
Java 层 AbstractQueuedSynchronizer (Java) |
|---|---|---|
| 实现语言与位置 | OpenJDK 源码 C++(objectMonitor.cpp / hpp) |
java.util.concurrent.locks 包下的标准 Java 类 |
| 管程状态载体 | Mark Word 指针 + ObjectMonitor 内部 _recursions / _owner |
32 位 volatile int state + exclusiveOwnerThread |
| 核心队列模型 | 三队列协同 :_cxq (栈) + _EntryList (双向链表) + _WaitSet (双向环形) |
双队列分离:Sync Queue (CLH 变体双向队列) + Condition Queue (单向链表) |
| 自旋锁策略 | 强依赖 自适应自旋 (Adaptive Spinning),根据历史成功率动态调整 | 无内置自适应自旋,入队前仅尝试少量 CAS,失败即挂起 (park) |
| 唤醒与继承人机制 | 引入 _succ (Successor) 假想继承人概念,降低惊群效应 |
依据 head.next 节点显式唤醒,采用 Tail-to-Head 逆向扫描容错 |
| OS 线程挂起 | os::PlatformEvent::park() / 线程间条件变量 |
LockSupport.park() → \rightarrow → Unsafe.park() |
二、 队列结构深度对比
1. ObjectMonitor 的三队列架构
ObjectMonitor 内部维护了三个独立且功能分工极明确的队列,节点数据结构为 ObjectWaiter。
┌───────────────────────┐
│ _cxq (Stack/LIFO) │ <── 多线程 CAS 无锁并发压入
└───────────┬───────────┘
│ (按 QMode 转移)
▼
┌───────────────────────┐
│ _EntryList (Buffer) │ <── 准备获取锁的就绪队列
└───────────┬───────────┘
▲
│ wait() / notify()
▼
┌───────────────────────┐
│ _WaitSet (Circular) │ <── 调用 wait() 挂起的等待队列
└───────────────────────┘
C++ 源码数据结构 (objectMonitor.hpp):
cpp
class ObjectMonitor {
markOop _header; // 保存原始对象头的 Displaced Mark Word
void* volatile _owner; // 当前持有锁的 JavaThread* 或 LockRecord*
volatile intptr_t _recursions; // 重入计数器
ObjectWaiter* volatile _cxq; // 多线程竞争入队的单向栈 (Contention Queue)
ObjectWaiter* volatile _EntryList; // 准备获取锁的双向链表 (Lock Entry List)
ObjectWaiter* volatile _WaitSet; // 调用 wait() 后挂起的双向环形链表
Thread* volatile _succ; // 继承人线程 (假想被指定唤醒的候选者)
int _SpinDuration; // 自适应自旋时长
};
_cxq(Contention Queue) :单向无锁栈。当多线程通过 CAS 抢占_owner失败时,会通过 CAS 头插法(LIFO)将当前线程的ObjectWaiter结点压入_cxq。_EntryList(Lock Entry List) :锁释放时的缓冲区。在锁释放(exit)时,持有锁的线程会将_cxq中的结点按一定规则(由Knob_QMode决定)转移到_EntryList中,避免每次唤醒都去竞争_cxq栈顶。_WaitSet(Wait Set) :双向环形链表。调用Object.wait()的线程会被封装为ObjectWaiter并追加到_WaitSet末尾,当被notify()唤醒后,再从_WaitSet转移到_cxq或_EntryList。
2. AQS 的双队列架构(CLH 变体 + ConditionObject)
AQS 将锁竞争队列 与条件变量等待队列解耦为两套物理独立的链表。
Sync Queue (CLH 变体)
[ Dummy Head ] <=====> [ Node 1 ] <=====> [ Node 2 (tail) ]
(head) ▲
│ CAS 入队
[ New Node ]
Condition Queue (ConditionObject)
[ Node A ] ───────> [ Node B ] ───────> [ Node C (lastWaiter) ]
(firstWaiter)
Java 源码数据结构 (AbstractQueuedSynchronizer.java):
java
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态标志
private transient volatile Node head; // 同步队列头结点(Dummy Node)
private transient volatile Node tail; // 同步队列尾结点
static final class Node {
volatile int waitStatus; // 结点状态:CANCELLED(1), SIGNAL(-1), CONDITION(-2), PROPAGATE(-3)
volatile Node prev; // 前驱结点(用于安全出队与逆向扫描)
volatile Node next; // 后继结点
volatile Thread thread; // 封装的线程引用
Node nextWaiter; // 用于 ConditionObject 单向链表或共享锁标记
}
}
-
同步队列 (Sync Queue / CLH 变体):
-
双向带头结点的链表 。
head永远是一个哑节点(Dummy Node),它的thread为null。 -
显示显式依赖前驱结点 :AQS 的核心逻辑是"前驱结点负责唤醒后继结点 "。后继结点入队后,必须将前驱结点的
waitStatus通过 CAS 改为SIGNAL (-1),表明"当我释放锁时,我必须唤醒你"。 -
条件队列 (Condition Queue):
-
在
ConditionObject内部定义的单向链表 (利用Node.nextWaiter连接)。 -
当调用
await()时,线程直接在单向链表尾部追加CONDITION (-2)状态的节点;当调用signal()时,将节点从单向条件队列头部摘下,通过enq()重新放入 AQS 的双向同步队列末尾。
三、 CAS 抢锁机制与自旋逻辑对比
1. ObjectMonitor 的 CAS 与自适应自旋 (Adaptive Spinning)
ObjectMonitor 的抢锁在 C++ 的 enter() 与 EnterI() 函数中完成。
cpp
// OpenJDK 8: src/share/vm/runtime/objectMonitor.cpp
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD;
// 1. 快速路径 (Fast Path):直接 CAS 抢锁
void * cur = Atomic::cmpxchg_ptr(Self, &_owner, NULL);
if (cur == NULL) return; // 抢锁成功
// 2. 重入检查 (Reentrancy)
if (cur == Self) {
_recursions++;
return;
}
// 3. 自适应自旋 (Adaptive Spinning)
if (Knob_SpinLimit > 0) {
if (TrySpin(Self) > 0) {
return; // 自旋期间抢锁成功,无需挂起
}
}
// 4. 自旋失败,进入慢速路径 EnterI(),入队并 park
EnterI(Self);
}
自适应自旋 (Adaptive Spinning) 的底层机理:
HotSpot 引擎引入了 TrySpin() 算法,其自旋次数不是固定的,而是基于历史统计:
- 如果对于同一个
ObjectMonitor,前几次自旋成功获取到了锁,JVM 会认为这次自旋成功的概率也很高,从而增加本次自旋的循环次数(例如从 500 次提升到 2000 次)。 - 反之,如果该锁上频频自旋失败,JVM 将减少自旋次数,甚至直接跳过自旋,立即挂起线程,防止 CPU 在无意义的空转中白白消耗电能和算力。
2. AQS 的 CAS 与抢锁逻辑
AQS 的抢锁分为独占模式 (如 ReentrantLock)与共享模式 (如 CountDownLatch)。以独占锁 acquireQueued 为例:
java
// java/util/concurrent/locks/AbstractQueuedSynchronizer.java
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 只有当自己的前驱节点是 Head 时,才有资格再次 CAS 抢锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 抢锁成功,自己成为新的 Dummy Head
p.next = null; // help GC
failed = false;
return interrupted;
}
// 检查并更新节点状态,准备挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}
AQS 抢锁特点:
- 严格的自旋限制 :AQS 入队后没有 复杂的 CPU 空转自旋。线程只有在
p == head(即自己是队列中的第一个等待节点)时才会在循环中再次调用tryAcquire。如果前驱节点不是head,直接触发parkAndCheckInterrupt()挂起。 - 公平锁与非公平锁控制:
- 非公平锁 :刚来的线程在未入队前直接插队调用
compareAndSetState(0, 1),抢占成功则直接抢锁;抢占失败才入队。 - 公平锁 :调用
hasQueuedPredecessors(),若队列中已有等待节点,即使 state 为 0,也强制入队排队。
四、 唤醒策略 (Unpark) 与线程继承机制对比
锁释放后的唤醒策略,决定了系统在并发恢复时的上下文切换开销与 CPU 调度效率。
1. ObjectMonitor 的 exit() 与 _succ 继承人机制
当线程调用 ObjectMonitor::exit() 释放锁时,并不是简单地唤醒下一个线程,而是结合了 Knob_QMode 控制策略 和 _succ 抢占缓冲。
cpp
// OpenJDK 8: src/share/vm/runtime/objectMonitor.cpp
void ATTR ObjectMonitor::exit(bool LockNode, TRAPS) {
Thread * Self = THREAD;
// 重入计数递减
if (_recursions > 0) {
_recursions--;
return;
}
for (;;) {
// 1. 先将 owner 释放 (置 NULL)
OrderAccess::release_store_ptr(&_owner, NULL);
OrderAccess::fence(); // 保证写屏障生效
// 2. 如果没有等待线程,或者已经有继承人 (_succ) 在自旋,直接退出!
if ((_cxq | _EntryList) == NULL || _succ != NULL) {
return;
}
// 3. 根据 QMode 选择从 _cxq 或 _EntryList 中挑选 Candidate
ObjectWaiter * w = NULL;
int QMode = Knob_QMode;
if (QMode == 2 && _cxq != NULL) {
w = _cxq; // Direct unpark cxq head
ExitEpilog(Self, w);
return;
}
// ... 默认 QMode=0: 如果 _EntryList 为空,将 _cxq 的节点移动到 _EntryList 中
if (_EntryList == NULL) {
_EntryList = _cxq;
_cxq = NULL;
// 倒置或整理链表...
}
w = _EntryList;
if (w != NULL) {
ExitEpilog(Self, w); // 内部会设置 _succ 并 unpark 线程
return;
}
}
}
_succ (Successor) 机制设计意图:
在高并发场景下,唤醒一个 OS 线程(unpark)存在很高的内核态切换延迟。ObjectMonitor 引入了 _succ 变量:
- 释放锁的线程在调用
unpark()唤醒候选线程w之前,将_succ指向w。 - 如果此时有新的新来线程进入
enter(),发现_succ != NULL,它就知道"已经有一个线程正准备被唤醒并接管锁了"。 - 此时新来线程就会积极自旋而不是盲目挂起,从而极大提升了锁在用户态流转的吞吐量。
2. AQS 的 release() 与 Tail-to-Head 逆向扫描
AQS 的唤醒逻辑在 release() → \rightarrow → unparkSuccessor() 中实现。
java
// java/util/concurrent/locks/AbstractQueuedSynchronizer.java
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 默认寻找头结点的下一个节点 (head.next)
Node s = node.next;
// 如果 s 为空或者 s 的状态 > 0 (CANCELLED)
if (s == null || s.waitStatus > 0) {
s = null;
// 关键逻辑:从尾节点 tail 开始往前逆向扫描,找到离 head 最近且 waitStatus <= 0 的有效节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 调用 Unsafe.unpark 唤醒线程
}
AQS 为什么必须采用"从尾到头 (Tail-to-Head)"逆向扫描?
这是一个非常经典的并发设计细节。在 AQS 的 enq() 入队操作中:
java
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { ... } // 初始化 Dummy Head
else {
node.prev = t;
if (compareAndSetTail(t, node)) { // Step 1: CAS 设为 tail
t.next = node; // Step 2: 建立前驱指向后继的 next 指针
return t;
}
}
}
}
由于 compareAndSetTail(t, node)(Step 1)和 t.next = node(Step 2)不是原子操作:
当 CAS 成功后,node.prev = t 已经建立,但前驱节点的 t.next 尚未赋值(此时 t.next 可能为 null)。
如果此时释放锁的线程刚好执行 unparkSuccessor,顺着 head.next 向前找,就会因为断链而漏掉刚入队的节点。但是 prev 指针是绝对可靠的 ,因此 AQS 必须从 tail 开始向前逆向扫描,确保不会遗漏任何有效节点。
五、 响应中断与并发取消 (Cancellation) 机制
1. AQS 的严格中断与取消状态处理
AQS 对响应中断与超时控制(如 tryAcquireNanos)有着原生的精细支持。
- 当等待线程因中断唤醒或超时时,会调用
cancelAcquire(Node node)。 - 节点状态被标记为
CANCELLED (1)。 cancelAcquire会通过 CAS 将断开的prev指针重新连接,跳过取消节点,并在下次shouldParkAfterFailedAcquire中彻底将CANCELLED节点从双向链表中剔除(GC 回收)。
2. ObjectMonitor 的撤销与中断处理
synchronized是不可中断的锁(进入enter()后不能被Thread.interrupt()中断强行退出);- 只有调用
Object.wait()处于_WaitSet时,才支持响应InterruptedException。如果线程在等待队列中被中断,JVM 同样需要将节点从_WaitSet搬运回_EntryList重新抢锁,抢到锁后再抛出InterruptedException。
六、 终极总结与选择权衡
JVM 的 ObjectMonitor 与 Java 的 AQS 呈现了两种截然不同的系统工程设计哲学:
ObjectMonitor(C++):
- 设计目标 :作为
synchronized关键字的底座,追求吞吐量极大化 与极低的单线程开销。 - 工程手段 :深度结合 JVM Mark Word 锁膨胀机制,采用三队列分流 降低 CAS 冲突,利用自适应自旋 让锁尽量留在用户态,通过
_succ继承人机制缓冲 OS 唤醒延迟。
AQS(Java):
- 设计目标 :作为 JUC 并发包的基石,追求灵活可扩展性 与可控的语义控制。
- 工程手段 :采用干净优雅的 CLH 变体双向队列 ,将锁状态统一抽象为
state整数,提供公平/非公平、独占/共享模式,原生支持中断响应、超时机制以及多个条件变量(ConditionObject)。
七、 全维度对比总结与选型启发
1. 深度对比矩阵
| 特性维度 | JVM ObjectMonitor (synchronized) |
Java AQS (ReentrantLock 等) |
|---|---|---|
| 代码实现 | C++ 源码,直接编译为机器码,集成在 JVM 内部 | 纯 Java 实现,依赖 Unsafe 的 CAS 与 Park 原语 |
| 队列数据结构 | _cxq(LIFO) + _EntryList(双向) + _WaitSet(双向) |
CLH 队列(双向 FIFO) + Condition 队列(单向 FIFO) |
| 抢锁自旋机制 | 自适应自旋 (根据历史成功率动态调整) | 有限固定自旋 (通常仅 1-2 次后直接 park) |
| 线程阻塞/唤醒 | 原生 os::PlatformEvent::park() |
LockSupport.park() → \rightarrow → Unsafe.park() |
| 上下文切换开销 | 较高,发生重量级锁竞争时直接切入 C++ / OS 系统调用 | 较低,Java 层面逻辑处理完毕后再调用 Unsafe |
| 响应中断/超时 | synchronized 不支持响应中断与超时(非阻塞) |
tryLock(time, unit) / lockInterruptibly() 完美支持 |
| 高级特性 | 支持偏向锁、轻量级锁(JDK 8 中)、锁消除、锁粗化 | 支持公平锁/非公平锁、共享锁/独占锁、多 Condition 队列 |
2. 总结
ObjectMonitor的设计哲学是"极致的吞吐量与性能" :它通过偏向锁/轻量级锁避免膨胀,膨胀后通过 LIFO 栈 (_cxq) 减少多线程 CAS 冲突,利用自适应自旋在 CPU 层面消化竞争,并通过强插队机制最大化 CPU 利用率,牺牲了公平性换取了极致性能。AQS的设计哲学是"高度的抽象性与扩展性" :它摒弃了复杂的 C++ 锁膨胀流程,将"同步状态管理"抽象为state,将"排队唤醒"抽象为 CLH 队列,为 Java 开发者提供了可定制(公平/非公平、共享/独占、中断响应)的强大同步器框架。