【JUC】synchronized 锁升级
synchronized的锁升级是 JVM 在 JDK 1.6 中引入的核心优化其目标是在不同竞争强度下自动选择开销最小的同步策略避免一上来就使用代价高昂的重量级锁。理解锁升级关键要抓住两条主线对象头 Mark Word 中标志位的变化以及用户态CAS/自旋与内核态Monitor 阻塞/唤醒之间的权衡。为什么要“升级”先理解用户态与内核态重量级锁依赖操作系统的Monitor互斥量实现。线程竞争失败时需要从用户态陷入内核态被阻塞释放锁时又要被唤醒这一来回涉及上下文切换代价很高。但 HotSpot 作者发现大多数同步场景其实并没有激烈竞争要么是一个线程反复进入要么是多个线程“错峰”执行。如果每次都直接走内核态阻塞完全是浪费。于是 JVM 设计了逐级增强的锁状态先用最轻的方式试试竞争加剧了再逐级“升级”。锁的状态保存在对象头中具体是Mark Word里。锁升级的本质就是Mark Word在不同竞争条件下被改写为不同的结构。Mark Word 与四种锁状态在 64 位 HotSpot 中Mark Word占 8 字节64 bit。它的后 3 位是锁标志位是区分锁状态的关键锁状态Mark Word 后 3 位存储内容概要无锁001hashcode、分代年龄、偏向标志位0偏向锁101偏向线程 ID、epoch、分代年龄轻量级锁00指向线程栈中 Lock Record 的指针重量级锁10指向 ObjectMonitor 的指针关键点Mark Word是非固定结构同一块内存根据锁状态被“复用”来存不同的信息。锁升级的过程就是这块内存内容被逐级替换的过程。⚠️JDK 版本差异偏向锁在 JDK 15 中已被标记为废弃JDK 18 起默认禁用。如果你的运行环境是较新版本偏向锁可能不会出现锁会从无锁直接走向轻量级锁。下面的原理以支持偏向锁的版本为主来讲解同时会标注新版本的变化。第一阶段偏向锁——单线程的“免检通道”适用场景同一个线程反复进入同一个同步块几乎无竞争。典型如单例的初始化、单线程循环中的同步操作。核心思想既然总是同一个线程来那就在对象头上记下它的名字线程 ID下次它再来时核对一下 ID 即可放行连 CAS 都不需要。加锁过程线程首次进入同步块JVM 通过一次 CAS 将Mark Word的偏向标志位置为1线程 ID 写入Mark Word锁标志位变为101。该线程再次进入时只需比对Mark Word中的线程 ID 是否等于自己。相等就直接进入无需任何原子操作。释放锁时偏向锁并不会主动撤销线程 ID 继续保持等待下次重入。偏向锁的撤销当另一个线程尝试获取这个偏向锁时偏向状态被打破。JVM 会等到一个安全点检查原偏向线程是否还存活、是否还在同步块中如果原线程已不再使用该锁可将对象恢复为无锁或重新偏向新线程如果原线程仍在同步中则撤销偏向锁升级为轻量级锁。代价对比偏向锁的初始化需要一次 CAS但之后的重入零 CAS 开销轻量级锁每次加锁/解锁都至少各一次 CAS。因此偏向锁在单线程重复场景下优势明显。偏向锁验证代码JOLimportorg.openjdk.jol.info.ClassLayout;publicclassBiasedLockDemo{privatestaticfinalObjectLOCKnewObject();publicstaticvoidmain(String[]args)throwsInterruptedException{// 建议加参数-XX:UseBiasedLocking -XX:BiasedLockingStartupDelay0System.out.println( 刚创建可偏向无锁状态 );printHeader(LOCK);synchronized(LOCK){System.out.println( 主线程第一次进入写入偏向线程ID );printHeader(LOCK);// 观察后3位应为 101}System.out.println( 释放后偏向状态保持 );printHeader(LOCK);// 仍然是 101线程ID未清除// 新线程竞争触发偏向撤销 → 轻量级锁ThreadtnewThread(()-{synchronized(LOCK){System.out.println( 新线程竞争升级为轻量级锁 );printHeader(LOCK);// 后3位应变为 00}});t.start();t.join();System.out.println( 偏向撤销后最终状态 );printHeader(LOCK);// 通常已不可再偏向}privatestaticvoidprintHeader(Objectobj){System.out.println(ClassLayout.parseInstance(obj).toPrintable());}}运行要点需添加--add-opens java.base/java.langALL-UNNAMEDJDK 9若在新 JDK 上运行偏向锁可能不可用观察到的状态会直接从001走向轻量级锁。第二阶段轻量级锁——用“自旋”代替“阻塞”当偏向锁被撤销或多个线程交替/轻度竞争同一个锁时JVM 进入轻量级锁状态。适用场景竞争线程数量不多且持锁时间很短。既然锁很快就会释放与其让等待线程进内核态阻塞再唤醒开销巨大不如让它在用户态“原地转圈”等一下——这就是自旋spin。加锁过程核心是 CAS 与 Lock Record线程在自己的栈帧中创建一块Lock Record将对象当前的Mark Word拷贝一份存进去称为 Displaced Mark Word。线程用CAS尝试将对象头的Mark Word替换为指向自己 Lock Record 的指针。CAS 成功线程获得轻量级锁Mark Word后 3 位变为00执行同步代码。CAS 失败说明已有其他线程持有锁。当前线程开始自旋循环重试 CAS。如果自旋达到一定次数或自旋线程数超过阈值仍然失败轻量级锁膨胀为重量级锁进入内核态阻塞。解锁过程持有锁的线程用 CAS 将Mark Word恢复为 Lock Record 中保存的原始值。若 CAS 恢复失败说明期间已有其他线程把锁升级/修改了则说明锁已膨胀需要走重量级锁的释放流程。轻量级锁的局限自旋会空耗 CPU。如果锁竞争激烈、持锁时间长大量线程长时间自旋反而比直接阻塞更伤性能此时升级为重量级锁是更合理的选择。轻量级锁验证代码JOLimportorg.openjdk.jol.info.ClassLayout;publicclassLightweightLockDemo{privatestaticfinalObjectLOCKnewObject();publicstaticvoidmain(String[]args){// 关闭偏向锁直接观察轻量级锁-XX:-UseBiasedLockingSystem.out.println( 初始无锁 );printHeader(LOCK);// 001synchronized(LOCK){System.out.println( 持有轻量级锁 );printHeader(LOCK);// 00Mark Word 指向栈上的 Lock Record}System.out.println( 释放后恢复无锁 );printHeader(LOCK);// 回到 001}privatestaticvoidprintHeader(Objectobj){System.out.println(ClassLayout.parseInstance(obj).toPrintable());}}如果开启了偏向锁但尚未被偏向第一次synchronized可能先进入偏向锁若想稳定观察轻量级锁可在启动参数中禁用偏向锁-XX:-UseBiasedLocking或使用一个新线程制造轻量竞争。第三阶段重量级锁——交给操作系统兜底当轻量级锁自旋失败达到阈值或者多个线程同时激烈竞争时锁膨胀为重量级锁。实现载体ObjectMonitor。对象头的Mark Word被设置为指向一个ObjectMonitor的指针后 3 位变为10。ObjectMonitor内部维护着Owner当前持锁线程、EntryList竞争失败被阻塞的线程队列等结构。行为未抢到锁的线程不再自旋而是被挂起阻塞进入EntryList持锁线程释放时从队列中唤醒一个等待线程阻塞与唤醒都涉及用户态↔内核态切换开销大但不会空耗 CPU适合高竞争、长临界区场景。重量级锁验证代码JOLimportorg.openjdk.jol.info.ClassLayout;publicclassHeavyweightLockDemo{privatestaticfinalObjectLOCKnewObject();publicstaticvoidmain(String[]args)throwsInterruptedException{// 用两个线程制造竞争使轻量级锁膨胀Threadt1newThread(()-{synchronized(LOCK){try{Thread.sleep(300);}catch(InterruptedExceptionignored){}}});Threadt2newThread(()-{// 先让 t1 拿到锁try{Thread.sleep(50);}catch(InterruptedExceptionignored){}synchronized(LOCK){System.out.println( t2 进入时锁已膨胀为重量级 );printHeader(LOCK);// 后3位应为 10}});t1.start();t2.start();t1.join();t2.join();}privatestaticvoidprintHeader(Objectobj){System.out.println(ClassLayout.parseInstance(obj).toPrintable());}}实际膨胀时机受 JVM 自适应自旋策略影响可以通过观察Mark Word后 3 位是否为10来确认重量级锁状态。锁升级的完整路径与不可逆性无锁 (001) → 偏向锁 (101) → 轻量级锁 (00) → 重量级锁 (10)触发升级的条件偏向锁被其他线程竞争 → 升级轻量级锁轻量级锁自旋失败/竞争加剧 → 升级重量级锁。锁升级是单向的不可降级。一旦升级为重量级锁后续即使竞争消失该对象也不会“退回”到轻量级或偏向锁状态。JVM 参数偏向锁可通过-XX:UseBiasedLocking开启旧版本默认开启-XX:-UseBiasedLocking关闭-XX:BiasedLockingStartupDelay0可消除偏向锁的启动延迟。总结三种锁的本质区别维度偏向锁轻量级锁重量级锁核心手段比对线程 ID免 CASCAS 自旋操作系统 Monitor线程状态无额外开销用户态自旋消耗 CPU内核态阻塞不耗 CPU适用场景单线程重复进入少量线程、短临界区多线程激烈竞争、长临界区主要代价初始一次 CAS自旋空转上下文切换一句话理解锁升级JVM 先用最便宜的“认脸”偏向锁不行就**“原地等一会儿”轻量级自旋再不行才“叫操作系统来排队”重量级阻塞**——逐级加码用最小的代价换取线程安全。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →