Java锁机制之Java对象重量级锁源码剖析 Java对象重量级锁源码剖析前言Java对象重量级锁源码剖析一、 ObjectMonitor::EnterI 核心源码分析二、 多线程并发“挤压” _cxq 的演进全过程1. 第一阶段并发乐观读取2. 第二阶段硬件级 CAS 决胜3. 第三阶段冲突缓解与分支重试Retry Mitigate三、深度架构思考为什么要这样设计四、关键技术点深度总结前言本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限文中内容难免存在疏漏恳请读者不吝指正Java对象重量级锁源码剖析在 HotSpot 虚拟机中当多个线程同时竞争 Java 对象的重量级锁ObjectMonitor失败时它们会被驱逐到慢速路径Slow Path中。ObjectMonitor::EnterI就是处理线程因锁饱和而需要封装、排队并挂起的核心方法。在这里涉及到一个关键的无锁低开销数据结构_cxqContention Queue竞争队列。它是一个无锁的、单向的LIFO后进先出链表。所有刚进入慢速路径、还未获得锁的线程称为 Recently Arrived Threads简称 RATs都会并发地通过 CAS 指令“挤入”这个队列的头部。一、ObjectMonitor::EnterI核心源码分析以下是 OpenJDK 8 中ObjectMonitor::EnterI方法中关于线程封装并挤压进入_cxq链表的核心代码片段已为你高度还原并补充了底层系统级工程师视角的详尽注释voidATTRObjectMonitor::EnterI(TRAPS){Thread*SelfTHREAD;// 检查并尝试获取锁如果成功则直接返回if(TryLock(Self)0)return;// 延迟初始化管程的相关数据DeferredInitialize();// 再次尝试自旋获取锁万一这时候锁被释放了呢if(TrySpin(Self)0)return;// // 【核心排队逻辑多线程并发挤压进入 _cxq 队列】// // 1. 栈上分配 ObjectWaiter 节点将当前线程Self包装其中// 这样做非常巧妙因为当前线程抢不到锁即将被挂起其栈帧Stack Frame在整个挂起期间都是绝对安全的// 这完美免去了在堆内存Heap中申请节点的巨大吞吐开销和垃圾回收压力。ObjectWaiternode(Self);// 初始化当前线程的 ParkEvent管程挂起与唤醒的核心系统级内核事件对象Self-_ParkEvent-reset();// 由于 _cxq 是单向链表只需要使用 next 指针。// 这里故意将 prev 指针设为一个非法死地址0xBAD用于 Debug 阶段防御性断言。node._prev(ObjectWaiter*)0xBAD;// 显式标记该节点当前所处的锁状态TS_CXQ代表其正在 _cxq 队列中等待node.TStateObjectWaiter::TS_CXQ;ObjectWaiter*nxt;// 2. 进入无锁死循环CAS 自旋直到成功将自己“挤压”进 _cxq 单向链表的头部for(;;){// 【步骤 A乐观读取】// 获取当前最新的 _cxq 链表头节点并将其赋值给当前临时变量 nxt同时让当前节点的 _next 指向它。// 这相当于让当前节点在本地做好准备隐式地成为新的头节点并指向老头节点。node._nextnxt_cxq;// 【步骤 B硬件级原子 CAS 替换】// 调用 Atomic::cmpxchg_ptr 进行原子替换。在 OpenJDK 8 中其参数含义依次为// 参数 1: node - 准备写入的新值即当前线程节点在栈上的地址// 参数 2: _cxq - 要修改的目标内存地址ObjectMonitor 对象中的 _cxq 指针// 参数 3: nxt - 预期中的旧值我们在【步骤 A】中乐观读取到的老头节点地址//// 底层行为CPU 会拦截并验证当前内存中的 *_cxq 是否仍然等于 nxt。// 如果等于未变说明期间没有其他线程干扰成功将 _cxq 指向 node并返回旧值 nxt。// 如果不等于已变说明有其他并发线程捷足先登“挤”了进来此时不修改内存返回实际的最新的 _cxq 值。if(Atomic::cmpxchg_ptr(node,_cxq,nxt)nxt){// 返回值等于预期旧值说明 CAS 成功当前节点顺利成为 _cxq 的新头部安全破出死循环。break;}// 【步骤 C并发冲突缓解与“贪婪”抢锁优化】// 如果走到这里说明上述 CAS 失败了即 Atomic::cmpxchg_ptr 返回的值 ! nxt。// 这代表刚才发生了多线程“挤压”冲突。在重新回到循环顶部进行下一次 CAS 冲锋前// HotSpot 引入了一个极其强悍的启发式优化再次调用 TryLock 尝试偷锁。if(TryLock(Self)0){// 如果运气爆棚在这里抢锁成功则直接退出 EnterI 方法// 此时该线程连队列都不用进了更不需要调用昂贵的系统调用去挂起park极大地提升了吞吐量。return;}// 偷锁失败继续循环重新读取最新的 _cxq 头部发起下一次排队冲锋。}// 后续逻辑进入等待被唤醒的阻塞状态...}二、 多线程并发“挤压”_cxq的演进全过程为了更直观地理解多线程是如何通过Atomic::cmpxchg_ptr挤压该单向链表的我们假设一个具体的并发场景当前_cxq队列中已经有一个老节点Node_Old。此时有三个线程Thread_A、Thread_B、Thread_C同时由于抢锁失败并发进入了EnterI的无锁死循环中。1. 第一阶段并发乐观读取三个线程在各自的 CPU 核心上并行执行到node._next nxt _cxq;。此时它们都在各自核心的寄存器/局部变量nxt中存下了当前的队列头Node_Old。并且它们各自栈上的节点指针也都指向了Node_OldnodeA._next Node_Old;nodeB._next Node_Old;nodeC._next Node_Old;2. 第二阶段硬件级 CAS 决胜三个线程几乎同时发起了Atomic::cmpxchg_ptr汇编指令在 x86 架构下底层会转换为带有lock cmpxchg前缀的单条硬件指令该指令会触发 MESI 缓存一致性协议的独占锁或锁住总线。Thread_A 动作最快它的 CPU 核心率先抢占了对_cxq内存行的修改权。CPU 发现此时内存里的_cxq的确等于Node_Old于是成功将_cxq的值修改为nodeA。Thread_A 的 CAS 宣告成功它顺利 break 调出循环。Thread_B 紧随其后发起 CAS它的指令去比对_cxq是否等于它预期的Node_Old。然而此时内存中的_cxq已经被 Thread_A 改成了nodeA。CPU 判定nodeA ! Node_Old因此Thread_B 的 CAS 宣告失败内存不作修改。Thread_C 同样发起 CAS它预期的旧值也是Node_Old与现在的真实值nodeA不符Thread_C 的 CAS 也宣告失败。3. 第三阶段冲突缓解与分支重试Retry MitigateThread_B 和 Thread_C 由于 CAS 失败不会立刻死板地重新排队而是先去执行TryLock探测锁是否恰好空闲。假设锁依然被别人占用TryLock失败。Thread_B 和 Thread_C 重新回到for(;;)循环的顶部。此时它们重新读取_cxqnodeB._next nxt _cxq;- 此时读取到的nxt变成了nodeA。接下来Thread_B 和 Thread_C 将在新一轮的nodeA头节点基础上重复上述的挤压竞争直到成功将自己变为新的全局_cxq头部。三、深度架构思考为什么要这样设计从系统和架构的角度来看HotSpot 在这段“挤压”逻辑中展现了极致的性能调优哲学栈上分配Stack Allocation免去 GC 开销传统的链表队列通常需要在堆上new ObjectWaiter()。而 HotSpot 直接在线程的执行栈Execution Stack上创建局部变量ObjectWaiter node(Self)。由于当前线程抢不到锁即将被挂起它的当前栈帧Stack Frame处于冰冻状态绝对不会被销毁。这完美利用了线程生命周期避免了频繁分配和回收节点的开销。LIFO 结构完美契合单指针 CAS为什么_cxq设计成后进先出LIFO而不是先进先出FIFO因为将节点插入到单向链表的头部Head Push只需要变更一个_cxq全局指针。如果实现无锁的 FIFO通常需要同时维护Head和Tail两个指针在并发环境下会引发复杂的双指针同步与 ABA 问题其算法复杂度和硬件锁竞争会呈指数级上升。“贪婪”的临门一脚Greedy TryLock在无锁编程中CAS 失败意味着高密度的并发冲突。传统的教科书做法通常是直接重试或者退避Backoff。而 HotSpot 却在这里插入了一个TryLock。这是一个非常有价值的启发式优化当发生冲突时意味着时间流逝了一小段此时原本持有锁的那个老线程可能刚好执行完了同步块并释放了锁。如果能在这个间隙“顺手牵羊”拿到锁就能完美避免后续昂贵的内核态切换Thread Park成本。四、关键技术点深度总结头插法LIFO 栈结构_cxq本质上是一个无锁栈。新来的线程总是通过修改_cxq指针把自己“挤”到最前面成为新的头节点。这就是为什么 Java 的重量级锁在某些激进场景下呈现出非公平性后来的线程反而可能先被唤醒因为它们在栈顶。硬件级锁保证Atomic::cmpxchg_ptr在底层依赖具体的 CPU 指令例如 x86 架构下的lock cmpxchg。它会锁定北桥总线或触发 MESI 缓存一致性协议确保“读取-比较-替换”这三个步骤在硬件层面是不可分割的单步原子操作。天然免疫 ABA 问题在通用的无锁队列中由于内存释放与复用常常需要引入版本号来解决 ABA 问题。但在 HotSpot 的EnterI中ObjectWaiter node是分配在线程私有栈上的局部变量。这意味着只要该线程没有退出EnterI方法这个内存地址绝不可能被其他线程复用。因此HotSpot 在这里不需要任何额外的版本号或 Epoch 机制非常干净利落地完成了无锁高并发链表的操作。