JUC包的AQS与JVM的ObjectMonitor机制对比分析

AQS与JVM的ObjectMonitor机制对比分析

  • 前言
  • AQS与JVM的ObjectMonitor机制对比分析
    • [一、 核心架构对比概览](#一、 核心架构对比概览)
    • [二、 队列结构深度对比](#二、 队列结构深度对比)
      • [1. `ObjectMonitor` 的三队列架构](#1. ObjectMonitor 的三队列架构)
        • [C++ 源码数据结构 (`objectMonitor.hpp`):](#C++ 源码数据结构 (objectMonitor.hpp):)
      • [2. AQS 的双队列架构(CLH 变体 + ConditionObject)](#2. AQS 的双队列架构(CLH 变体 + ConditionObject))
        • [Java 源码数据结构 (`AbstractQueuedSynchronizer.java`):](#Java 源码数据结构 (AbstractQueuedSynchronizer.java):)
    • [三、 CAS 抢锁机制与自旋逻辑对比](#三、 CAS 抢锁机制与自旋逻辑对比)
      • [1. `ObjectMonitor` 的 CAS 与自适应自旋 (Adaptive Spinning)](#1. ObjectMonitor 的 CAS 与自适应自旋 (Adaptive Spinning))
        • [自适应自旋 (Adaptive Spinning) 的底层机理:](#自适应自旋 (Adaptive Spinning) 的底层机理:)
      • [2. AQS 的 CAS 与抢锁逻辑](#2. AQS 的 CAS 与抢锁逻辑)
        • [AQS 抢锁特点:](#AQS 抢锁特点:)
    • [四、 唤醒策略 (Unpark) 与线程继承机制对比](#四、 唤醒策略 (Unpark) 与线程继承机制对比)
      • [1. `ObjectMonitor` 的 `exit()` 与 `_succ` 继承人机制](#1. ObjectMonitorexit()_succ 继承人机制)
        • [`_succ` (Successor) 机制设计意图:](#_succ (Successor) 机制设计意图:)
      • [2. AQS 的 `release()` 与 Tail-to-Head 逆向扫描](#2. AQS 的 release() 与 Tail-to-Head 逆向扫描)
        • [AQS 为什么必须采用"从尾到头 (Tail-to-Head)"逆向扫描?](#AQS 为什么必须采用“从尾到头 (Tail-to-Head)”逆向扫描?)
    • [五、 响应中断与并发取消 (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 语言层面 ReentrantLockSemaphore 等并发工具的基石)均采用了 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),它的 threadnull

  • 显示显式依赖前驱结点 :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 抢锁特点:
  1. 严格的自旋限制 :AQS 入队后没有 复杂的 CPU 空转自旋。线程只有在 p == head(即自己是队列中的第一个等待节点)时才会在循环中再次调用 tryAcquire。如果前驱节点不是 head,直接触发 parkAndCheckInterrupt() 挂起。
  2. 公平锁与非公平锁控制
  • 非公平锁 :刚来的线程在未入队前直接插队调用 compareAndSetState(0, 1),抢占成功则直接抢锁;抢占失败才入队。
  • 公平锁 :调用 hasQueuedPredecessors(),若队列中已有等待节点,即使 state 为 0,也强制入队排队。

四、 唤醒策略 (Unpark) 与线程继承机制对比

锁释放后的唤醒策略,决定了系统在并发恢复时的上下文切换开销与 CPU 调度效率。

1. ObjectMonitorexit()_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 呈现了两种截然不同的系统工程设计哲学:

  1. ObjectMonitor (C++)
  • 设计目标 :作为 synchronized 关键字的底座,追求吞吐量极大化极低的单线程开销
  • 工程手段 :深度结合 JVM Mark Word 锁膨胀机制,采用三队列分流 降低 CAS 冲突,利用自适应自旋 让锁尽量留在用户态,通过 _succ 继承人机制缓冲 OS 唤醒延迟。
  1. 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 开发者提供了可定制(公平/非公平、共享/独占、中断响应)的强大同步器框架。
相关推荐
霸道流氓气质1 小时前
SpringBoot中使用OAuth2 认证与 JWT Token — 概念、原理与实践
java·spring boot·后端
dogstarhuang1 小时前
Kimi K3 本地部署实战:从 1.56TB 权重到推理服务的完整成本分析
java·人工智能·后端·ai·开源·接口·程序员创富
就不掉头发1 小时前
C++函数模板
java·开发语言·c++
用户298698530141 小时前
Word 转 PDF 的 3 种自动化实现:从桌面操作到后端服务集成
java·人工智能·后端
wuyk5551 小时前
89.嵌入式内存管理的“稳定利器”:内存池原理与C语言实现
c语言·开发语言·stm32·单片机·嵌入式硬件
闲云自留地1 小时前
课后作业-20260806-Linux iSCSI服务器
linux·运维·服务器
Zane19942 小时前
线程池进阶:如何科学设置线程数(结合实际业务场景复盘)
java·后端
SeaTunnel2 小时前
Apache SeaTunnel 提交一个任务都经过了什么?
java·大数据·服务器·apache·etl·seatunnel
mifengxing2 小时前
Java TreeSet
java·开发语言