重量级锁ObjectMonitor 详解

10 阅读5分钟

我们深入到 HotSpot 源码层面,详细拆解 ObjectMonitor 的核心属性在加锁、解锁、等待/通知过程中的协作机制。理解这些属性的交互是掌握重量级锁实现的关键。


一、ObjectMonitor 核心属性清单

属性类型作用
_ownervolatile Thread*当前持有锁的线程指针(无锁时为 NULL)
_recursionsint当前线程的重入次数(_owner 为当前线程时有效)
_cxqObjectWaiter*竞争队列(栈结构,LIFO),新到达的线程先入此队列
_EntryListObjectWaiter*等待队列(双向链表),存放等待获取锁的线程
_WaitSetObjectWaiter*等待集(双向链表),存放调用了 wait() 的线程
_countint用于记录 _EntryList 中等待线程数(调试/统计)
_succvolatile Thread*启发式优化:暗示下一个可能获得锁的线程,减少不必要的唤醒
_ResponsibleThread*用于 try 块或超时等待时的“责任线程”
_SpinDurationint自适应自旋的时长基数
_WaitSetLockint保护 _WaitSet 的简单自旋锁

本文重点讲解 _owner_recursions_cxq_EntryList_WaitSet 五个核心属性的工作流程。


二、加锁流程(enter 方法)

当线程调用 monitorenter 进入同步块时,最终会调用 ObjectMonitor::enter(TRAPS)

1. 快速路径(Fast Path)

Thread * const self = THREAD;
void * cur = Atomic::cmpxchg(&_owner, (void*)NULL, self);
if (cur == NULL) {
    // CAS 成功,获得锁
    _recursions = 1;
    return;
}
if (cur == self) {
    // 重入
    _recursions++;
    return;
}
  • 第一步:尝试 CAS 将 _owner 从 NULL 改为当前线程。成功则锁获取完成,_recursions 置 1。
  • 第二步:如果 _owner 已经是当前线程,则增加 _recursions,实现重入。

2. 自旋(Spin)

如果快速路径失败,且 _owner 是其他线程,JVM 会进行自适应自旋(默认开启):

  • 通过 TrySpin 函数自旋一段时间,期望锁很快释放。
  • 自旋过程中如果 _owner 变为 NULL 且 CAS 成功,则获得锁。
  • 自旋成功可避免线程阻塞/唤醒的开销。

3. 慢速路径(Slow Path)—— 入队与阻塞

如果自旋仍未获得锁,线程进入入队阻塞流程:

ObjectWaiter node(self);     // 封装当前线程
node._TState = ObjectWaiter::TS_CXQ;   // 状态:在 cxq 中

// 将 node 放入 _cxq 头部(栈操作)
for (;;) {
    node._next = _cxq;
    if (Atomic::cmpxchg(&_cxq, node._next, &node) == node._next)
        break;
}
  • 每个等待锁的线程被封装为 ObjectWaiter 节点。
  • 新节点总是插入 _cxq 的头部(LIFO),这样做是为了让新来的线程更有可能在锁释放时被唤醒,减少饥饿。
  • 放入 _cxq 后,线程调用 park() 挂起,进入 BLOCKED 状态。

为什么需要 _cxq_EntryList 两个队列?

  • _cxq 是新竞争线程的“快速入口”,采用 LIFO 减少 CAS 冲突。
  • _EntryList_cxq 的“稳态”队列,用于批量转移和唤醒。
  • 解锁时可以根据策略从 _cxq_EntryList 中唤醒线程,实现不同的公平性/吞吐量权衡。

三、解锁流程(exit 方法)

当线程执行 monitorexit 时,调用 ObjectMonitor::exit(TRAPS)

1. 减少重入计数

if (_recursions != 0) {
    _recursions--;      // 还有重入,不释放锁
    return;
}
  • 如果 _recursions > 0,仅递减计数,锁仍然被当前线程持有。

2. 释放锁并唤醒下一个线程

_recursions == 0 时,真正释放锁:

OrderAccess::release_store(&_owner, (void*)NULL);   // 1. 清空 owner
OrderAccess::fence();                               // 2. 写屏障,保证可见性

// 3. 如果 _cxq 或 _EntryList 不为空,需要唤醒后继线程
if (_EntryList != NULL || _cxq != NULL) {
    // 根据 QMode 策略选择从哪个队列取线程
    ObjectWaiter *w = NULL;
    if (QMode == 1) {
        // 将 _cxq 全部转移到 _EntryList 尾部,再从 _EntryList 头部唤醒
        w = _EntryList;
    } else if (QMode == 2) {
        // 直接从 _cxq 头部唤醒(LIFO,后进先出)
        w = _cxq;
    }
    // ... 其他 QMode 值(0,3,4)有不同策略
    if (w != NULL) {
        // 将 w 从队列中取出,调用 unpark() 唤醒该线程
        unpark(w->_thread);
    }
}

关键点:

  • QMode 是 JVM 参数(-XX:QMode=0/1/2/3/4),控制唤醒策略:
    • QMode=0(默认):先尝试从 _EntryList 唤醒,如果为空则从 _cxq 取并移入 _EntryList
    • QMode=1:将 _cxq 整体转移到 _EntryList 尾部,再唤醒 _EntryList 头部(FIFO,较公平)。
    • QMode=2:直接从 _cxq 头部唤醒(LIFO,新来的线程先获得锁,可能导致饥饿)。
  • 唤醒的线程会从之前 park() 的位置恢复执行,重新尝试 CAS 获取锁。

四、wait/notify 机制与 _WaitSet

_WaitSet 负责管理调用了 wait() 的线程。

1. wait() 流程(ObjectMonitor::wait)

// 前提:当前线程持有锁(_owner == self)
_recursions++;                    // 保存重入计数
int save_recursions = _recursions;
// 释放锁:将 _owner = NULL,并且唤醒 _EntryList/_cxq 中的等待者
exit(true, self);                 // 释放锁

// 将当前线程封装为 ObjectWaiter,状态设为 TS_WAIT
ObjectWaiter node(self, ObjectWaiter::TS_WAIT);
// 插入 _WaitSet 链表尾部
_WaitSetLock.lock();
node._next = _WaitSet;
_WaitSet = &node;
_WaitSetLock.unlock();

// 挂起当前线程(通过 park())
self->park();

// 当被 notify 或 超时/中断 唤醒后,重新竞争锁
// 重新执行 enter() 获取锁,恢复 _recursions
_owner = self;
_recursions = save_recursions;
  • 线程进入 _WaitSet 后,不再参与锁竞争,直到被 notify/notifyAll 移出。
  • wait() 会释放锁,让其他线程有机会进入同步块。

2. notify() 流程(ObjectMonitor::notify)

// 从 _WaitSet 头部取出一个节点
ObjectWaiter *w = _WaitSet;
if (w != NULL) {
    // 将 w 从 _WaitSet 中移除
    _WaitSet = w->_next;
    // 修改状态为 TS_ENTER,准备参与锁竞争
    w->_TState = ObjectWaiter::TS_ENTER;
    // 将 w 插入 _cxq 头部(LIFO)或 _EntryList 尾部,取决于 QMode
    // 默认插入 _cxq,让新唤醒的线程以 LIFO 方式竞争
    w->_next = _cxq;
    _cxq = w;
}
  • notify 仅将线程从 _WaitSet 移到竞争队列(_cxq_EntryList),不会立即唤醒它。
  • 被移动的线程将在锁释放后有机会被 unpark() 唤醒并重新竞争锁。

五、属性交互全景图

graph TD
    A[新竞争线程] -->|enter| B{快速CAS抢锁?}
    B -->|成功| C[_owner = thread, _recursions=1]
    B -->|失败| D{重入?}
    D -->|是| E[_recursions++]
    D -->|否| F[自适应自旋]
    F -->|成功| C
    F -->|失败| G[封装为ObjectWaiter<br/>插入_cxq头部]
    G --> H[park 阻塞]
    H --> I[等待被 unpark]

    J[解锁线程] -->|exit| K[_recursions--]
    K -->|仍有重入| L[返回]
    K -->|归零| M[清空 _owner]
    M --> N[根据QMode选择<br/>从_cxq或_EntryList取节点]
    N --> O[unpark 唤醒线程]
    O --> H

    P[线程调用 wait] --> Q[保存 _recursions<br/>释放锁 exit]
    Q --> R[插入 _WaitSet]
    R --> S[park 等待 notify]

    T[线程调用 notify] --> U[从 _WaitSet 取出节点]
    U --> V[节点移入 _cxq 或 _EntryList]
    V --> W[后续被 exit 唤醒]

六、总结:核心属性的职责划分

属性核心职责典型工作场景
_owner标识锁的当前持有者加锁时 CAS 设置,解锁时清空
_recursions支持可重入同一线程多次 enter 时递增,exit 时递减
_cxq新竞争线程的快速入口慢速路径入队,解锁时按策略取出
_EntryList等待线程的稳态队列配合 QMode 实现公平或吞吐优先的唤醒
_WaitSet管理 wait() 的线程wait 时入队,notify 时移出到 _cxq/_EntryList

这些属性协同工作,既保证了锁的互斥语义,又通过双队列(_cxq + _EntryList)和 QMode 策略在公平性吞吐量之间做了权衡。理解这套机制,就能明白为什么重量级锁在竞争激烈时依然能保持稳定,以及 wait/notify 必须与 synchronized 配合使用的底层原因。