当前位置: 首页 > news >正文

Java锁机制之非公平锁源码剖析

非公平锁源码剖析

  • 前言
  • 非公平锁源码剖析
    • 一、 ObjectMonitor 的双队列核心架构
    • 二、 核心内核源码精读:ObjectMonitor::exit
    • 三、 彻底搞懂:两个核心设计意图的微观拆解
      • 1. Java 重量级非公平锁在 C++ 内核中如何靠“插队(Barging)”实现?
      • 2. 深入理解 `Knob_QMode` 的“队列反转”与微观拓扑变化
        • 策略 0:默认策略(不公平的延续)
        • 策略 1:队列反转(走向公平的微观魔法)
        • 策略 2:越级唤醒
    • 四、 总结

前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正

非公平锁源码剖析

在 Java 重量级锁的底层实现中,ObjectMonitor(对象监视器)是核心。要彻底搞懂 Java 非公平锁在 C++ 内核中如何通过“插队(Barging)”“队列反转(Queue Inversion)”实现高吞吐量与特定公平策略,必须深入剖析hotspot/src/share/vm/runtime/objectMonitor.cpp中的ObjectMonitor::exit方法。


一、 ObjectMonitor 的双队列核心架构

在理解exit逻辑之前,必须先明确ObjectMonitor内部的两个核心等待队列:

  1. _cxq(Contention Queue - 竞争队列):
  • 数据结构:单向无界链表栈(LIFO)。
  • 特性:当多个线程同时竞争锁失败时,它们会通过 CAS(无锁原子操作)将自己压入_cxq头部。这意味着最新到来的线程永远在_cxq的最前端。
  1. _EntryList(进入队列):
  • 数据结构:双向链表(通常表现为 FIFO 倾向,但取决于具体整顿策略)。
  • 特性:专门由释放锁的线程(Current Owner)负责维护。引入_EntryList的核心目的是为了分流,避免唤醒线程的操作与正在高频执行 CAS 入队的竞争线程在同一个_cxq头节点上发生激烈的 CPU 缓存行冲突(False Sharing)。

二、 核心内核源码精读:ObjectMonitor::exit

以下是 OpenJDK 8 中ObjectMonitor::exit的核心剥离版本,重点聚焦于Knob_QMode的选择与队列操作,并附带详尽的系统级注释。

voidATTRObjectMonitor::exit(boolnot_suspended,TRAPS){Thread*Self=THREAD;// 1. 偏向锁/轻量级锁升级后的所有权校验if(THREAD!=_owner){if(THREAD->is_lock_owned((address)_owner)){_owner=THREAD;// 如果是当前线程曾拥有的轻量级锁升级而来,修正所有权}else{// 非法调用异常处理(如未持有锁直接调用 notify/wait/exit)if(ThrowIllegalMonitorStateException){VM_THROW(vmSymbols::java_lang_IllegalMonitorStateException());}return;}}// 2. 处理可重入锁的减计数if(_recursions!=0){_recursions--;return;// 重入计数未归零,直接返回,当前线程继续持有锁}// 3. 进入主死循环,开始准备释放锁并选择后继唤醒线程for(;;){// 根据不同的退出策略(默认 Knob_ExitPolicy == 0),先释放锁,再决定唤醒谁if(Knob_ExitPolicy==0){// 【核心插队点】将 _owner 置为 NULL,释放锁所有权。// 此时,外部全新的线程如果执行 ObjectMonitor::enter,可以通过 CAS 一枪爆头直接夺锁成功!// 这就是 Java 层面“非公平锁”中,新来线程可以直接“插队”抢锁的本质原因。OrderAccess::release_store_ptr(&_owner,NULL);OrderAccess::fence();// 全局内存屏障,强制刷新 StoreBuffer,确保多核可见性// 快速路径:如果当前没有线程在等待(_cxq 和 _EntryList 都为空),或者已经存在明确的后继唤醒目标(_succ)// 则当前线程完美退出,什么都不用管。if(((_cxq|_EntryList)==NULL)||_succ!=NULL){return;}// 关键防漏看(Lost Wakeup)保护机制:// 锁虽然释放了,但发现队列里有等待者,且此时没有指定的后继者(_succ == NULL)去唤醒它们。// 当前退出线程必须“被迫”重新通过 CAS 夺回锁的所有权,由它来扮演“守门人”,去队列里挑人唤醒。if(Atomic::cmpxchg_ptr(Self,&_owner,NULL)!=NULL){// 如果重新夺锁失败,说明有外部线程(比如刚才插队的线程)已经把锁抢走了。// 既然新 Owner 已经产生,新 Owner 在它未来 exit 的时候会负责唤醒队列,当前线程可以安心退出了。return;}}// 走到这里,说明当前线程成功“复辟”重新拿到了锁,现在开始从队列中挑选幸运儿ObjectWaiter*w=NULL;intQMode=Knob_QMode;// 获取 JVM 内部调优参数 QMode// ====================================================================// Knob_QMode 核心处理逻辑开始// ====================================================================// 【QMode == 2】:极其激进的绝对非公平策略(LIFO)// 绕过 _EntryList,直接去嗅探 _cxq。因为 _cxq 是最新进来的线程在头部,// 如果直接唤醒 _cxq 的 head,意味着最晚阻塞的线程最先被唤醒,极大压榨了冷线程,换取极高的热吞吐。if(QMode==2&&_cxq!=NULL){w=_cxq;assert(w!=NULL,"invariant");ExitEpilog(Self,w);// 唤醒 _cxq 顶部的线程,直接返回return;}// 【QMode == 3】:批量插队策略(将 _cxq 整体拦截并移到 _EntryList 的头部)if(QMode==3&&_cxq!=NULL){w=_cxq;_cxq=NULL;// 瞬间通过单核原子语义清空 _cxqwhile(w!=NULL){ObjectWaiter*q=w->_next;w->TState=ObjectWaiter::TS_ENTER;// 经典头插法:遍历整个 _cxq,顺次插入到 _EntryList 的头部// 注意:由于是顺次头插,这导致 _cxq 内部的 LIFO 顺序在移交到 _EntryList 头部后被“再次反转”w->_next=_EntryList;w->_prev=NULL;if(_EntryList!=NULL)_EntryList->_prev=w;_EntryList=w;w=q;}// 迁移完毕后,不在此处唤醒,继续向下走常规的 _EntryList 唤醒流程}// 【QMode == 4】:温和追加策略(将 _cxq 整体尾插到 _EntryList 尾部)// 这种策略维护了相对的先后顺序,老等待者(_EntryList)依旧享有高优先级。if(QMode==4&&_cxq!=NULL){w=_cxq;_cxq=NULL;// 找到 _EntryList 的尾节点,把 _cxq 的头挂上去,代码略(标准双向链表尾插)}// 【常规选择】:如果经过上述处理,或者本来 _EntryList 中就有残留线程w=_EntryList;if(w!=NULL){assert(w->TState==ObjectWaiter::TS_ENTER,"invariant");ExitEpilog(Self,w);// 唤醒 _EntryList 的头节点return;}// 到这一步,说明 _EntryList 全空。必须从 _cxq 中“搬运”线程过来填补空缺w=_cxq;if(w==NULL)continue;// 如果此时 _cxq 也空了,说明刚才看走眼了(被插队线程带走了),重新循环// 用一个无锁 CAS 死循环,把整个 _cxq 的链表一次性“一网打尽”拉取出来,并将 _cxq 置空for(;;){assert(w!=NULL,"invariant");ObjectWaiter*res=(ObjectWaiter*)Atomic::cmpxchg_ptr(NULL,&_cxq,w);if(res==w)break;// 成功截获完整的 _cxq 链表,退出 CAS 循环w=res;// 失败说明有新的怨种线程在入队,更新 w(获取最新栈顶)继续尝试}// 【QMode == 1】:纯正的“队列反转(Queue Inversion)”以实现公平化// 此时 w 指向原 _cxq 的头(即最新到达的线程),其指向链表深处的是老线程(LIFO)。if(QMode==1){ObjectWaiter*n=NULL;ObjectWaiter*p=w;// 核心单向链表反转算法:// 将 A -> B -> C -> D 翻转为 D -> C -> B -> A// 翻转后,原先处于栈底的 D(最早等待的线程)变成了翻转后链表的头 n。while(p!=NULL){ObjectWaiter*c=p->_next;p->_next=n;n=p;p=c;}_EntryList=n;// 将最早到达的线程置于 _EntryList 头部// 为翻转后的新 _EntryList 重建双向链表的 _prev 指针ObjectWaiter*q=NULL;p=_EntryList;while(p!=NULL){p->TState=ObjectWaiter::TS_ENTER;p->_prev=q;q=p;p=p->_next;}}else{// 【QMode == 0】(JVM 默认策略):不翻转队列,直接挂载!// 直接把 w(原 _cxq 栈顶,即最新到达的线程)无缝接管为 _EntryList 的头节点。_EntryList=w;ObjectWaiter*q=NULL;ObjectWaiter*p=w;while(p!=NULL){p->TState=ObjectWaiter::TS_ENTER;p->_prev=q;q=p;p=p->_next;// 保持了原先的 LIFO 拓扑顺序}}// 既然队列整顿好了,最后从最新的 _EntryList 中挑出第一个元素唤醒if(_EntryList!=NULL){w=_EntryList;ExitEpilog(Self,w);return;}}}

三、 彻底搞懂:两个核心设计意图的微观拆解

1. Java 重量级非公平锁在 C++ 内核中如何靠“插队(Barging)”实现?

Java 的synchronized重锁是非公平的,其核心推力来自于exit先放锁、后唤醒的时序差。

  • 时序窗口:当一个线程在exit中执行完OrderAccess::release_store_ptr(&_owner, NULL)后,锁其实已经自由了。
  • 新线程切入:此时,另一个刚执行到ObjectMonitor::enter的新线程(可能刚在 Java 层面执行到synchronized(lock))在 CPU 上欢快地奔跑。它根本不需要进队列,而是直接执行了一句Atomic::cmpxchg_ptr(Self, &_owner, NULL)
  • 抢占成功:只要这个 CAS 成功,新线程就成功“插队”截获了锁。
  • 唤醒线程的尴尬:此时,在exit内部继续执行的旧持锁线程回头一看:“咦,锁怎么又被人抢走了?”。它通过cmpxchg重新尝试抢锁失败,只能无奈退出。而被它之前选定或者正准备选定的后继线程(_succ),哪怕被ExitEpilog通过系统调用(如pthread_mutex_unlockos::PlatformEvent::unpark())唤醒后,起来第一件事也是去抢_owner,结果发现锁早就被插队线程拿走了,它只能叹口气,再次进入挂起状态或继续自旋。

内核设计哲学:允许外部新线程“插队”能极大提升系统的整体吞吐量(Throughput)。因为新线程正处于 CPU 核心的热执行状态(寄存器、L1/L2 缓存全部命中),直接让它拿锁往下跑的开销,远远小于将一个在 OS 内核层挂起的冷线程唤醒并进行上下文切换(Context Switch)的开销。

2. 深入理解Knob_QMode的“队列反转”与微观拓扑变化

_EntryList为空,必须去搬运_cxq时,Knob_QMode的不同取值决定了锁对“已排队线程”的公平程度。

策略 0:默认策略(不公平的延续)
  • 微观操作:直接令_EntryList = _cxq
  • 后果:由于_cxq是个 LIFO 栈,最新进去的线程在栈顶。直接挂载意味着_EntryList的头节点就是最后阻塞的那个线程。因此,在默认策略下,不仅外部新来的能插队,内部排队的线程也是“后到先得”,公平性降到最低,但换取了极高的高频锁竞争效率。
策略 1:队列反转(走向公平的微观魔法)
  • 微观操作:显式执行单向链表反转(Inversion)。

  • 拓扑演变:假设线程按照T1 -> T2 -> T3的顺序到达并竞争失败。

  • 由于 CAS 入栈特性,_cxq的物理拓扑为:[Head] -> T3 -> T2 -> T1 -> NULL

  • QMode == 1触发反转后,拓扑变为:[Head] -> T1 -> T2 -> T3 -> NULL

  • 最终_EntryList指向了T1

  • 后果:T1是最早到来的线程,它现在处于_EntryList的头部,将被优先唤醒。通过队列反转,JVM 在内部将一个 LIFO 结构的竞争队列,巧妙地转换为了 FIFO 结构的唤醒队列,保障了“先来后到”的排队公平性。

策略 2:越级唤醒
  • 微观操作:连把_cxq搬运到_EntryList的伪装步骤都省了,只要_cxq不为空,抓起_cxq的头节点直接调用ExitEpilog唤醒。
  • 后果:只要高并发下_cxq不断有新怨种线程加入,_EntryList里的老线程可能面临无限期饥饿(Starvation)。

四、 总结

通过对ObjectMonitor::exit源码的精读,可以提炼出 Java 重量级非公平锁的核心精髓:

  1. 宏观上的非公平:exit先释放_owner = NULL**制造空窗期,让外部热点 CPU 上的新线程通过 CAS 暴力插队**,免去线程唤醒与上下文切换成本,换取极致吞吐量。
  2. 微观上的策略调配:靠内部Knob_QMode的值控制队列拓扑。默认的QMode = 0保持_cxq的 LIFO 属性;而QMode = 1通过显式的指针反转,逆转了新老线程的宿命,实现了已排队线程间的 FIFO 公平性。
http://www.cnnetsun.cn/news/2806166.html

相关文章:

  • 从V5到V6:Rapid SCADA 6.0 在Linux(Ubuntu 22.04)上的平滑迁移与避坑实战
  • 如何高效配置多平台直播:OBS多RTMP推流插件实战指南
  • Matlab全变分图像去噪工程包:含TV算法核心代码、自适应参数模块与多组实测效果对比
  • 智慧医疗ACDC数据集MRI图像心梗扩张型心肌病肥厚型心肌病右心室病变识别分割数据集labelme格式1147张5类别
  • 三分钟彻底掌控Alienware:500KB轻量工具完全替代AWCC
  • STM32H7上跑ThreadX USBX?手把手教你搞定MDK/IAR开发环境与资源下载
  • 从欧·亨利《二十年后》看技术文档的‘承诺’与‘履约’:如何设计可靠的API接口契约?
  • AI写专著高效攻略:AI专著写作工具,3天搞定20万字专著撰写!
  • Zotero GPT终极指南:5分钟打造你的AI文献助手
  • OpenSpeedy:终极免费开源Windows游戏加速工具完整指南
  • 告别样式烦恼:用GeoServer的CSS插件和osm-styles项目,一键还原OpenStreetMap官方地图效果
  • TensorLayer实现的CVAE-GAN图像生成与双路径重建(含ResNet结构判别器+预训练权重)
  • 如何用Python自动化抢票脚本告别演唱会门票秒光烦恼
  • 用粒子群算法在MATLAB里自动找PID三个参数的最优解
  • 多维聚合实战:超越GROUP BY的数据操作核心
  • 掌握跨平台直播分发:obs-multi-rtmp插件深度应用指南
  • Wand-Enhancer终极教程:三步免费解锁Wand专业版完整功能
  • 从El Niño监测到气候研究:SLA/SSHA数据到底怎么用?给非遥感专业者的指南
  • 终极解决方案:如何一键安装Adobe插件?ZXPInstaller免费开源指南
  • Windows任务栏透明化神器:TranslucentTB终极使用指南
  • ComfyUI-Manager终极安装失败排查:Git环境变量配置深度解析与解决方案
  • 3个提升日常效率的Git实用技巧:状态增强、提交校验与日志语义化
  • GPT-4涌现能力解析:跨模态推理与自主工具调用的‘火花’实证
  • NS-USBloader:一站式解决Switch文件传输、RCM注入和文件管理三大难题
  • 用Python和OpenCV模拟维苏威火山喷发:一个数据可视化与地理信息系统的实战项目
  • Go 后端生产事故排障实战:基于 eBPF 的零侵入性能诊断
  • 不只是Root:用TWRP和Magisk解锁Pixel手机的更多玩法(模块、备份、系统修改)
  • Matlab差分演化算法DE实现:10个经典测试函数一键批量寻优
  • iPhone 屏蔽号码管理攻略:快速查找、解除与添加,常见问题解答
  • 变化检测实战:工业时序数据中的概念漂移识别与在线响应