尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

史上最细Synchronized实现原理

史上最细Synchronized实现原理 synchronized实际上有三种实现状态在锁升级过程中会依次从偏向锁、轻量级锁到重量级锁演进。下面会介绍这三种锁状态的实现方式。零、Mark Word后简称MWJava的synchronized基本上都是围绕这个Mark Word实现的Mark Word保存在对象头中用来标记当前的锁状态具体结构如下所示。JavaThread*表示线程的指针LockRecord*表示指向LockRecord栈中锁记录的指针ObjectMonitor*表示指向ObjectMonitor监视器的指针。需要注意的是物理内存中低位在右高位在左下面表格按照这个方向制作。锁状态bit 63~10(高位数据区)bit 9~8(重偏向时间戳)bit 7(未使用)bit 6~3(分代年龄)bit 2(偏向标志)bit 1~0(锁标志)无锁bit 63~41全0留空bit 40~10hashCode00分代年龄0 (未偏向)01(偏向锁或无锁)偏向锁JavaThread*所属线程的指针存放批量重偏向的批次值0分代年龄1 (已偏向)01(偏向锁或无锁)轻量级锁LockRecord*栈中锁记录的指针00(轻量级锁)重量级锁ObjectMonitor*ObjectMonitor锁监视器的指针10(重量级锁)通过MW判断锁状态的伪代码0b前缀表示二进制// 1. 原子读取整个 Mark Word mark object-mark(); // 2. 判断是否为偏向锁检查最低 3 位是否等于 101 if ((mark 0b111) 0b101) { // 偏向锁路径比较线程指针 if (mark.thread current_thread) { // 重入直接进入 } else { // 竞争触发偏向撤销STW } } // 3. 判断是否为轻量级锁检查最低 2 位是否等于 00 else if ((mark 0b11) 0b00) { // 轻量级锁路径CAS 自旋 } // 4. 判断是否为重量级锁检查最低 2 位是否等于 10 else if ((mark 0b11) 0b10) { // 重量级锁路径阻塞等待 } // 5. 剩下的情况lock01 且 biased_lock0无锁 else { // 无锁路径尝试 CAS 抢占 }一、偏向锁偏向锁的实现方式就是通过CAS方式将synchronized对象头的MW修改为当前线程的指针线程重入该锁时不需要再执行CAS只需要比较MW是否为当前线程ID。对于长期只有一个线程获取的锁对象来说这种偏向锁的性能消耗是非常低的。具体过程如下在初始状态时锁对象的头MW标志位biased_lock为0未偏向lock标志位为01偏向锁101表示可偏向满足伪代码的else条件使用CAS将高位数据区置为线程指针同时将biased_lock置位1。后续该线程再进入该锁时只要判断Mark Word后三位为标记为101则直接比较高位是否指向当前线程是则获取锁成功。需要注意的是这个MD操作就算线程释放锁也不会置为无锁状态这就是为什么叫偏向的锁从而不管是重入锁还是释放锁后重新进入都只需要比较MD带来较小的性能消耗。同时对于当前线程来说会在当前栈帧或栈顶中压入一个 Lock Record [ Lock Record_1: {obj锁对象, Displacednull} ]。第二次、第三次进入同一个 synchronized 块时锁重入JVM 不会再去修改对象头因为 ID 已经匹配。但在栈上JVM 会继续压入新的 Lock Record每个新记录依然是 obj 指向锁对象Displaced Mark Word null。这些记录的唯一作用就是“重入计数器”每次退出同步代码块弹出一个保证获取多少次就释放多少次只要还拥有一个Lock Record就认为线程仍然持有该锁这是为什么MD足已判断当前偏向锁为线程持有仍然需要压入Lock Record的原因。后续出现竞争时就可以通过这个判断是否线程还持有锁。Lock Record 字段存储内容说明obj锁对象引用指向锁对象的指针标记这个锁记录属于哪个对象。Displaced Mark Word置换标记字null空这是关键区别偏向锁不保存原Mark Word副本因为释放时不需要还原对象头对象头里的线程 ID 留着不擦除。通过上面的伪代码不难看出假设出现多个线程竞争则若继续使用偏向锁不断改变偏向的线程需要频繁STW这带来的消耗比CAS自旋还高所以如果出现竞争则偏向锁就会升级为轻量级锁。二、偏向锁升级为轻量级锁过程第一阶段检测到竞争T2 入场假设T1先获取了偏向锁现在T2也试图获取该对象的偏向锁首先T2 执行 monitorenter读取MW检查最低三位发现最低 3 位是 101判定当前处于偏向锁状态然后对比线程指针发现指向 T1而不是当前线程 T2。触发撤销JVM 判定发生了锁竞争不再允许 T2 进行 CAS 自旋因为在偏向锁协议下对象头没有自旋计数器和状态位而是立即进入 “偏向锁撤销Revocation” 流程若T1仍然使用该锁对象则在撤销后升级为轻量级锁若 T1 已退出则将对象头恢复为无锁状态biased_lock0, lock01后续 T2 可以重新尝试获取偏向锁相当于一次全新的偏向锁获取流程。第二阶段请求全局安全点STW这是偏向锁性能开销最大的来源。为了保证线程栈帧的稳定性JVM 必须暂停所有应用线程包括 T1 和 T2到达一个全局安全点Safe Point。为什么必须 STW 因为 JVM 需要遍历 T1 的调用栈查找 obj 对应的锁记录。如果不停顿 T1T1 的栈帧在不断变化方法入栈出栈遍历结果就不准确。第三阶段遍历 T1 栈帧查找 Lock Record在 STW 期间JVM 拿着锁对象 obj 的引用去线程 T1 的栈中执行全栈扫描遍历每一个栈帧检查每个栈帧中的 Lock Record锁记录列表查看是否有某个 Lock Record 的 obj 字段指向了当前这个 obj 对象。此时会出现两种结果升级为轻量级锁走的是情况 2情况 1T1 已退出不升级如果在 T1 栈中找不到匹配的 Lock Record说明 T1 早已执行完同步块只是对象头残留了 T1 的 ID偏向锁特性。此时 JVM 会将对象头恢复为无锁状态biased_lock0, lock01清空线程指针然后唤醒 T2让 T2 重新走“无锁抢锁”流程。情况 2T1 仍持有升级如果在 T1 栈中找到了匹配的 Lock Record说明 T1 依然在执行同步代码块锁确实被占用。此时JVM 执行升级操作。第四阶段膨胀为轻量级锁核心转换当确认 T1 仍持有锁后JVM 在 STW 期间执行以下原子转换在保留T1持有锁的情况下将锁结构从偏向锁转为轻量级锁。1.填充 T1 的 Lock Record将 T1 栈中那个匹配的 Lock Record 的 Displaced Mark Word字段偏向锁时为空填入当前对象头中原本的 Mark Word 的副本即包含 T1 线程指针和 age/epoch 的那串原始位数据。在轻量级锁升级为重量级锁时MD依然没有位置保留这部分内容所以会转移保存在 ObjectMonitor 的 _header 字段中。2.修改 Mark Word转换为轻量级锁结构将对象头的MD整体覆写最低 2 位设置为 00轻量级锁标志。高位2~63 位存储指向 T1 栈中那个 Lock Record 的指针62 位地址。此时biased_lock 位bit 2已经被指针覆盖不再独立存在。虽然对象头变成了轻量级锁但 Lock Record 属于 T1所以 JVM 依然认为 T1 是锁的持有者。T1 对此毫无感知它继续执行同步块内的代码。3.退出 STW所有线程恢复运行。第五阶段T2 自旋等待轻量级锁特性STW 结束后线程 T2 再次读取锁对象的MD发现最低两位是 00轻量级锁且高位指向了 T1 的栈帧。T2 知道锁被 T1 占用于是进入轻量级锁的自旋 重试流程在自己的栈帧中创建 Lock Record然后循环执行 CAS尝试将对象头的指针改为指向自己的 Lock Record。在 T1释放之前CAS 一直失败T2 会自旋消耗 CPU等待 T1 释放。直到 T1 退出同步块执行轻量级锁释放逻辑将对象头 CAS 还原为无锁状态T2 才有机会抢到锁。三、轻量级锁升级为重量级锁过程第一阶段触发膨胀决策自适应自旋失败T2 在自旋过程中JVM 的自适应自旋机制动态调整自旋次数如果自旋次数达到阈值或持有者 T1 长时间未释放锁如发生 GC 或 I/O 阻塞JVM 判定 “继续自旋的成功概率极低且浪费 CPU 资源”则触发膨胀Inflate指令JVM 不再允许 T2 继续自旋而是进入 ObjectSynchronizer::inflate() 方法正式启动升级流程。第二阶段分配 ObjectMonitor 并 CAS 抢占原子替换这是升级过程中最关键的竞态步骤JVM 必须保证仅有一个线程成功执行膨胀。执行 CAS 替换对象头T2尝试通过CAS操作将对象头的Mark Word从指向T1栈的指针修改为一个特殊的中转值 INFLATING。如果 CAS 成功当前线程T2成为“膨胀执行者”进入第三阶段作为锁升级的唯一打工人。如果 CAS 失败1. 持有者 T1 已释放锁对象回到无锁状态这是完全可能发生的。T1 可能执行速度极快在 T2 发起膨胀的瞬间已经完成了同步块并通过 CAS 将 Mark Word 成功还原为无锁状态01。此时T2 的 CAS 会因 Mark Word 已不是“指向 T1 栈”而失败。处理逻辑很清晰锁竞争已经消失T2 无需再膨胀而是直接尝试用 CAS 获取这个无锁对象即可然后重新走一遍轻量级锁的加锁流程。2. 其他线程已成功将锁膨胀为重量级锁这是另一个常见情况。如果另一个线程 T3 抢先一步完成了膨胀Mark Word 已经被改为重量级锁状态10指向一个 ObjectMonitor 对象。T2 的 CAS 会因此失败此时 T2 会退出膨胀直接使用这个现成的 ObjectMonitor进入ObjectMonitor的_cxq。3. 其他线程正在执行膨胀INFLATING 状态如果 T3 抢先将 Mark Word 改为了正在膨胀状态INFLATINGT2 的 CAS 同样会失败。这时 T2 会自旋等待不断重新读取 Mark Word直到T3 完成膨胀后变为 10重量级然后进入ObjectMonitor的_cxq。第三阶段迁移锁持有权T1 的栈信息 → MonitorT2线程在堆中申请一块 C 对象内存分配给 ObjectMonitor 对象并初始化其核心字段_owner null暂未拥有者_recursions 0重入计数归零_EntryList null阻塞队列为空_WaitSet null调用了Object.wait()的等待队列为空CAS 成功后执行者假设是 T2 抢到了 CAS需要将锁的持有信息迁移到 ObjectMonitor 其中就包括将锁重入次数从T1的栈Lock Record计数统计出来赋值给_recursions 。所以T2会通过MD指向的T1的锁记录主动遍历 T1 的栈帧注意此时对象头已被改为 INFLATING如果 T1 恰好执行 monitorexit会看到 INFLATING 并自旋等待因此 T1 的栈是稳定的T2 可以安全读取。T2 统计 T1 栈中所有指向该锁对象的 Lock Record 数量得到重入次数 count。然后填充 ObjectMonitor将 _owner 设置为 T1将 _recursions 设置为 count重入次数将Lock Record中保存的原 Mark Word 中的 hashCode/age 等保存到 _header 字段并将 _EntryList 初始化为空队列。至此ObjectMonitor初始化完成。最后T2 执行最后一次 CAS将对象头的MD从 INFLATING 修改为指向该 ObjectMonitor 的指针lock10然后T2通过 ObjectMonitor::enter() 进入_cxq 去排队。第四阶段自旋线程的迁移T2、T3 进入 _cxq当 T2 将对象头从INFLATING改为10后所有自旋等待的线程都会从自旋中醒来线程醒来后的动作T3竞争线程获取重新读取Mark Word发现是10重量级锁。立即退出inflate()复用这个现成的ObjectMonitor然后调用ObjectMonitor::enter()进入重量级锁竞争流程。T1持有者释放重新读取Mark Word发现是10重量级锁。放弃轻量级锁释放逻辑转而调用ObjectMonitor::exit()若重入计数减一后为0则将_owner置为null并唤醒_cxq/_EntryList中的等待线程。四、重量级锁的运作机制只有当前持有锁的线程_owner才能操作_EntryList。这是一个非常严格的规则确保了队列操作的独占性和安全性。1.线程进入_cxq队列当一个线程比如线程T2尝试获取已被占用的锁时会被封装成一个 ObjectWaiter 节点。这个节点会被优先放入 _cxq竞争队列 中-。_cxq 是一个无锁的、LIFO后进先出 的单向链表允许大量线程通过CAS操作快速、并发地进入减少了竞争。放入 _cxq 的线程会被挂起park请注意线程并不直接进入 _EntryList_EntryList 中的线程都是从 _cxq “转移”过来的。这个“转移”动作发生在持有锁的线程比如线程A释放锁时。当线程A执行完同步代码准备释放锁时它会检查 _EntryList 是否为空。如果为空但 _cxq 不为空线程A就会将整个 _cxq 队列转移到 _EntryList 中。2._cxq转移到_EntryList这是一个由锁的持有者线程执行的操作。_EntryList 是一个双向链表并且为了保证操作的简单和快速新转移来的节点通常被设计为以LIFO后进先出的方式插入到 _EntryList 的头部。_cxq 与 _EntryList 的区别特性_cxq(竞争队列)_EntryList(入口列表)数据结构无锁单向链表 (LIFO)双向链表-并发控制无锁并发 (CAS)仅锁持有者可操作线程状态刚到达已挂起等待被唤醒的挂起线程-主要目的快速入队减少竞争-有序出队便于持有者管理3.锁释放时如何唤醒线程当持有锁的线程A完全释放锁_recursions 减为0后需要唤醒下一个等待线程。这个唤醒顺序由 JVM 参数 QMode 控制。核心逻辑是优先从 _EntryList 中获取线程进行唤醒。常见的策略有默认QMode0如果 _EntryList 不为空则从 _EntryList 中取出一个线程唤醒-。如果 _EntryList 为空则将 _cxq 中的线程全部转移到 _EntryList再从其中唤醒一个。QMode1将 _cxq 中的线程转移到 _EntryList并唤醒 _EntryList 中的线程。QMode2直接唤醒 _cxq 中的线程不经过 _EntryList。其他存在更多复杂的策略甚至可以将 _EntryList 中的线程转移到 _cxq-。五、synchronized不同锁机制的核心区别synchronized锁升级顺序为 偏向锁 - 轻量级锁 - 重量级锁我们都知道性能消耗是随着锁升级越来越大的。通过上面的解析不难看出偏向锁在没有锁竞争时每次进入锁包括重入或者释放或重新进入都只需要比较MD的值这是性能最好的。轻量级锁需要并发线程不断地自旋尝试CAS多次自旋CAS需要消耗一些性能但是始终是用户态运行线程也始终没有挂掉所以次之。而重量级锁真正有了线程上下文切换的过程线程竞争时直接挂起等待后面又要被唤起这带来的内核调度性能消耗是最大的。
返回列表