重量级锁ObjectMonitor 详解

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


一、ObjectMonitor 核心属性清单

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

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


二、加锁流程(enter 方法)

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

1. 快速路径(Fast Path)

cpp 复制代码
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)------ 入队与阻塞

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

cpp 复制代码
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. 减少重入计数

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

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

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

cpp 复制代码
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)

cpp 复制代码
// 前提:当前线程持有锁(_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)

cpp 复制代码
// 从 _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
插入_cxq头部] G --> H[park 阻塞] H --> I[等待被 unpark] J[解锁线程] -->|exit| K[_recursions--] K -->|仍有重入| L[返回] K -->|归零| M[清空 _owner] M --> N[根据QMode选择
从_cxq或_EntryList取节点] N --> O[unpark 唤醒线程] O --> H P[线程调用 wait] --> Q[保存 _recursions
释放锁 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 配合使用的底层原因。

相关推荐
gelald2 小时前
JVM - 类加载机制
java·jvm·后端
96772 小时前
C++ 内存管理的核心——RAII 机制。两种锁 lock_guard, unique_lock
java·jvm·c++
Yupureki2 小时前
《Linux系统编程》18.线程概念与控制
java·linux·服务器·c语言·jvm·c++
庞轩px4 小时前
面试回答第十五问:类加载
jvm·面试·职场和发展·常量池·类加载·字节码·klass
ywf12154 小时前
java进阶1——JVM
java·开发语言·jvm
庞轩px15 小时前
模拟面试回答第十三问:JVM内存模型
jvm·面试·职场和发展
森林里的程序猿猿16 小时前
并发设计模式
java·开发语言·jvm
u01368638217 小时前
将Python Web应用部署到服务器(Docker + Nginx)
jvm·数据库·python
njidf18 小时前
实战:用Python开发一个简单的区块链
jvm·数据库·python