1. 先搞清楚一个前提JDK 1.6 之前的 Synchronized 为什么被人嫌弃聊锁升级之前得先把时间线拉回到十几年前。很多刚接触并发编程的同学翻老博客经常看到Synchronized 是重量级锁性能差别用这类结论然后转头又看到JDK 1.6 之后 Synchronized 性能大幅提升的说法直接给搞懵了到底哪个是对的答案是两个都对只是属于不同时代。在 JDK 1.5 及更早的版本里Synchronized 的实现完全依赖底层操作系统的 Mutex Lock互斥锁。线程进入 synchronized 代码块时如果锁被其他线程持有当前线程就会从用户态切换到内核态由操作系统帮忙阻塞挂起。这个切换过程涉及系统调用、线程状态保存与恢复开销非常大。换句话说哪怕你的临界区只是一行count一旦发生锁竞争线程就要被拖进内核走一趟耗时可能比执行那行代码本身高出几个数量级。这也是为什么那个年代涌现出一堆替代方案——自旋锁、偏向锁的雏形、各种自己手写的高级锁工具。Doug Lea 写的ConcurrentHashMap在 Java 5 里直接用分段锁规避竞争ReentrantLock则直接基于 AQS 在用户态完成大部分同步逻辑就是为了绕开 synchronized 的高昂开销。转折发生在 JDK 1.6。HotSpot 团队做了一次大手术给 synchronized 加上了偏向锁Biased Locking和轻量级锁Lightweight Locking配合原有的重量级锁形成一条由轻到重的升级链路。默认配置下锁会随着竞争激烈程度自动升级而不是一上来就拉出重量级锁的架势。加上 JIT 编译器的锁消除Lock Elimination和锁粗化Lock Coarsening优化synchronized 在无竞争或低竞争的常见场景下性能已经能和ReentrantLock掰手腕甚至在部分场景比如 JDK 8 的synchronized与ReentrantLock性能基准测试简直难分伯仲。锁升级的核心思想一句话概括大部分锁其实只在同一个线程里反复进出根本没有竞争那就别用昂贵的内核锁来保护它们了。不过这里我要先泼一盆冷水偏向锁在 JDK 15 里被标记为废弃JDK 18 之后默认关闭。这个变化后面会专门讲先不剧透——搞清楚来龙去脉比记住结论重要得多。2. 偏向锁为什么说它是连 CAS 都省了的锁2.1 偏向锁的设计动机先问一个问题在多线程程序里有多少锁是被真正两个以上线程竞争过的答案是少得可怜。绝大多数锁在绝大多数时间只被同一个线程持有——比如一个 ArrayList 在单线程里反复 add/remove比如线程池里的 Worker 线程处理任务时访问自己的本地缓存再比如一些经典的线程封闭场景。如果这些情况都走完整的加锁/解锁流程哪怕用的是轻量级锁也需要执行 CAS 原子操作这个操作在 x86 上有对应的cmpxchg指令虽然没有系统调用那么贵但也涉及内存屏障高频调用依然不该忽略。偏向锁的思路更激进如果这个锁从始至终只有一个线程访问那就连 CAS 都不做直接认为这个锁归这个线程所有。怎么做到的靠的是对象头里的 Mark Word。2.2 Mark Word 里的三位状态位要理解锁升级必须先把 HotSpot 虚拟机的对象内存布局摸清。一个 Java 对象在内存里主要包含三部分对象头Header、实例数据Instance Data、对齐填充Padding。对象头又分两部分Mark Word标记字和 Klass Pointer类型指针开启压缩指针后 4 字节。Mark Word 是锁升级的主战场。它是一块 64 位64 位 JVM 下的存储区根据对象状态的不同里面存的信息完全不一样。最低三位是锁标志位其中两位表示锁状态一位表示是否偏向锁状态标志位Mark Word 存储内容64 位 JVM 示意无锁001对象的 hashCode、分代年龄偏向锁101持有偏向锁的线程 ID、epoch、分代年龄轻量级锁000指向栈中锁记录的指针重量级锁010指向 Monitor监视器锁的指针GC 标记011空不参与锁升级过程注意一个细节无锁状态和偏向锁状态可以互相转换但一旦对象进入轻量级锁或重量级锁状态再回到无锁状态时原来的 hashCode 等信息可能已经丢了所以对同一个对象如果先调用了hashCode()再锁它偏向锁默认是可以用的但如果 hashCode 用的是延迟计算且已经算过那偏向锁可能直接被禁用——因为 Mark Word 里已经存了 hashCode没地方放线程 ID 了。这个现象后面在异常情况里会细说。2.3 偏向锁的获取与撤销流程偏向锁的获取过程可以概括为三查两不改一改定归属。当一个线程第一次访问 synchronized 块时JVM 会执行以下步骤检查 Mark Word 里的锁标志位是否为101偏向锁。如果不是说明锁处于其他状态走对应流程。如果是偏向锁检查 Mark Word 中存储的线程 ID 是否是当前线程。如果是说明线程已经持有该锁直接进入临界区不需要任何同步操作。这就是偏向锁最理想的情况一次加锁操作几乎零开销。如果线程 ID 不是当前线程说明有另一个线程也想拿这把锁。此时需要执行 CAS 操作尝试把 Mark Word 中的线程 ID 从原线程替换成当前线程。CAS 成功则说明原线程已经释放了偏向锁实际上偏向锁只有竞争发生时才会释放下面会说当前线程获得锁。如果 CAS 失败说明锁正在被另一个线程持有当前线程需要触发偏向锁撤销Revoke Bias。偏向锁的撤销是整个机制里最复杂、最反直觉的部分。为什么说反直觉因为偏向锁只有在有人来抢的时候才会释放。如果持有偏向锁的线程正常执行完退出同步块它不会主动修改 Mark Word 释放偏向锁——它默认反正也没别人来抢我懒得改。这种偷懒设计在单线程场景下节省了大量操作但一旦竞争出现就需要一个全局协调点来处理。偏向锁撤销的核心步骤如下当前线程竞争者发现偏向锁指向另一个线程于是向 JVM 提交撤销请求。JVM 会安全点Safe Point暂停持有偏向锁的线程——注意是持有线程不是竞争线程。检查持有线程是否还存活如果已经退出同步块或已死亡JVM 把 Mark Word 恢复为无锁状态001然后让竞争者重新走轻量级锁或偏向锁获取流程。如果还在同步块内活跃说明确实存在竞争JVM 将偏向锁升级为轻量级锁让两个线程公平竞争。恢复所有被暂停的线程继续执行。这个流程看着简单但牵扯到安全点停顿、线程状态遍历实际代价不低。更麻烦的是一次偏向锁撤销会影响同类的其他对象——这引出了批量重偏向和批量撤销机制。2.4 批量重偏向Bulk Rebias与批量撤销Bulk Revoke假设一个对象的偏向锁被撤销了一次JVM 会把这个对象的类标记一下。如果同一个类的对象频繁发生偏向锁撤销JVM 会启动批量重偏向当一个类的撤销次数达到阈值默认 20JVM 会认为这个锁实例存在竞争但同类其他对象可能仍适合偏向于是给这个类开启一个重偏向纪元epoch。epoch 记录在 Mark Word 的偏向锁字段里每次批量重偏向发生时epoch 1。如果一个偏向锁对象上的 epoch 与当前类的 epoch 不一致说明它是过期偏向其他线程可以直接 CAS 抢占——不需要走完整的撤销流程。如果撤销次数继续上升到阈值默认 40JVM 直接对该类的对象禁用偏向锁。所有新实例直接以无锁状态可走轻量级锁创建偏向锁对它们不再生效。这就是为什么很多资料说偏向锁撤销代价高高竞争场景下反而拖后腿——它省的是单线程场景的时间但省下的时间可能还不够一次撤销路费。JDK 15 废弃偏向锁核心原因之一就是这个收益模型在当代硬件和工作负载下不再划算。注意以上阈值20/40是 HotSpot 的默认值可通过-XX:BiasedLockingBulkRebiasThreshold和-XX:BiasedLockingBulkRevokeThreshold调整。一般生产环境不建议动知道存在就行。3. 轻量级锁与重量级锁的分水岭一次 CAS 的博弈3.1 轻量级锁的获取为什么自旋偏向锁一旦被撤销紧随其后的通常就是轻量级锁。轻量级锁的获取逻辑跟偏向锁相比多了一个关键动作在栈帧中创建锁记录Lock Record。流程拆开来看线程在执行 synchronized 块前在当前线程的栈帧中分配一块锁记录空间用于存放对象 Mark Word 的副本称为 Displaced Mark Word。通过 CAS 尝试把对象头中的 Mark Word 替换为指向锁记录的指针同时把原 Mark Word 值存到锁记录中。如果 CAS 成功说明当前线程拿到了锁Mark Word 的锁标志位变为00。线程进入临界区。如果 CAS 失败说明锁被别人持有了。此时当前线程不会立刻挂起而是执行**自旋Spin**操作——空转 CPU 循环等待锁释放重试 CAS。自旋的设计逻辑非常实际轻量级锁面向的是锁持有时间非常短的场景。如果持有锁的线程只是做几行计算就退出临界区那竞争线程等待的时间可能只有几微秒。与其让线程进入内核态阻塞再唤醒光是上下文切换可能就耗去几十微秒不如在用户态原地打转等对方释放后立刻抢到锁。但自旋有个天然缺陷如果锁持有时间太长自旋就变成了白白烧 CPU。于是 JDK 引入了自适应自旋Adaptive Spinning——JVM 根据上次在这个锁上的自旋成功率动态决定本次自旋多久上次自旋成功说明锁竞争不激烈适当延长旋上次自旋失败说明竞争激烈直接缩短甚至不旋赶紧进内核挂起。这个机制从 JDK 1.6 开始是默认开启的参数是-XX:UseSpinning虽然名字带个开关但实际运行中几乎可以认为自适应自旋是常驻的。3.2 轻量级锁的释放与膨胀轻量级锁的释放对称且简单线程退出同步块时把 Displaced Mark Word 通过 CAS 写回对象头。如果写回成功说明锁已正常释放如果写回失败说明有其他线程在自旋等待甚至已经把锁升走了这时候当前线程需要唤醒等待线程并完成锁膨胀。这里需要讲清楚容易混淆的一点自旋发生在轻量级锁获取阶段但多个线程自旋抢同一把轻量级锁最终只有一个能 CAS 成功其余线程怎么办答案是抢不到的线程会继续自旋一会儿到阈值后放弃进入锁膨胀流程。膨胀就是把轻量级锁升级为重量级锁的过程——JVM 会为对象创建一个 Monitor监视器/管程并把对象头的 Mark Word 指向这个 Monitor锁标志位变为10。后续竞争线程不再自旋而是直接通过 Monitor 的阻塞-唤醒机制等待。3.3 Monitor 内部到底存了什么很多人学过 AQSAbstractQueuedSynchronizer之后会对 Monitor 产生一种亲切感——因为它的结构和 AQS 有几分神似。HotSpot 里每个对象都关联一个 ObjectMonitorC 实现核心字段包括_owner持有锁的线程。_WaitSet调用wait()后进入等待状态的线程集合。_EntryList处于阻塞状态、等待获取锁的线程集合。_recursions锁的重入次数这就是 synchronized 可重入的底层依据。_count竞争计数器。重量级锁的加锁/解锁直接对应于 ObjectMonitor 的 enter/exit 方法。enter会尝试 CAS 设置_owner失败则把线程放入_EntryList并挂起exit则唤醒_EntryList中的线程。整个过程涉及线程状态切换和系统调度开销最大但换来了公平性和可控性——这是它作为兜底锁的价值。关于 Monitor 和 synchronized 的关系我见过不少误解。有人以为 synchronized 的可重入是 JVM 靠递归计数器实现的严格说重入计数确实在重量级锁的_recursions字段里但轻量级锁和偏向锁的重入纯粹是再走一遍流程——偏向锁重入只比较线程 ID轻量级锁重入直接判断锁记录指针已存在不需要额外计数。所以可重入这个语义在各锁状态下实现方式不同不能一概而论。4. 锁升级的完整触发路径从偏向到重量级的全过程4.1 一条主线走通全流程把前面各部分串起来锁升级完整流程可以用下面这条主线描述。假设初始是无锁状态线程 A 先来无锁001-- 线程A获取 -- 偏向锁101 线程A再次进入 -- 偏向锁101直接通过零开销 线程B尝试获取 - CAS失败 - 触发偏向锁撤销 偏向锁撤销成功线程B获取 -- 轻量级锁00 线程C、D、E同时竞争 -- 自旋失败 -- 锁膨胀 轻量级锁升级 -- 重量级锁10 后续线程全部通过 Monitor 阻塞/唤醒机制竞争这条路径有四个关键触发点大多是面试和实操中的高频考点偏向锁 → 轻量级锁发生在第二个线程尝试获取偏向锁且 CAS 失败时。注意第二个线程是尝试抢锁而不是想读数据所以偏向锁撤销本质是一个检测到多线程竞争的信号。轻量级锁自旋成功竞争线程在自旋期间等到了锁释放并成功 CAS轻量级锁状态保留不升级。这是最理想的多线程低竞争状态。轻量级锁自旋失败 → 膨胀多个线程持续竞争导致抢不到锁的线程放弃自旋触发重量级锁。膨胀完成后原先持有轻量级锁的线程在释放时写回 CAS 失败顺势唤醒_EntryList中的等待线程。重量级锁降级理论上存在但只在 STWStop-The-World的某些场景下有条件地发生比如偏向锁批量撤销后的特定路径常规业务运行中几乎不可见。所以面试里如果有人说锁升级不可逆你就知道常见的升级路径确实不可逆但底层实现留了降级的后门——实际上 synchronized 一旦膨胀为重量级锁同一把锁在当前 JVM 生命周期内基本不会自动降回轻量级 这个说法是被验证的。4.2 一个实际的实验复现纸上谈兵没意思我给一个验证锁升级的可执行思路。写一个简单类分别测三种场景通过打印对象头的二进制数据或者用 JFRJDK Flight Recorder观察锁状态变化。public class LockStateDemo { private static final Object obj new Object(); public static void main(String[] args) throws Exception { // 场景一单线程反复进入 synchronized 块 for (int i 0; i 5; i) { synchronized (obj) { System.out.println(单线程场景理应处于偏向锁); } } // 场景二两个线程交替进入但持有时间极短 Thread t1 new Thread(() - { synchronized (obj) { try { Thread.sleep(100); } catch (InterruptedException ignored) { } } }); Thread t2 new Thread(() - { synchronized (obj) { tester(双线程短持有); } }); t1.start(); t1.join(); t2.start(); t2.join(); } private static void tester(String label) { System.out.println(label); } }要看清 Mark Word 里锁状态位最直接的办法是用 JOLJava Object Layout这个工具它出自 JVM 大牛 Aleksey Shipilëv 之手# 在 pom.xml 中加入依赖 dependency groupIdorg.openjdk.jol/groupId artifactIdjol-core/artifactId version0.17/version /dependencyimport org.openjdk.jol.info.ClassLayout; // 在 synchronized 块内部和外部各打印一次 synchronized (obj) { System.out.println(ClassLayout.parseInstance(obj).toPrintable()); }打印结果里 Mark Word 那一行会看到类似0x000000002a1ef905的十六进制值。盯着最低三位看结尾101偏向锁结尾000轻量级锁结尾010重量级锁实测下来单线程循环场景几乎必是101双线程竞争后大概率变000或010。注意 JOL 的打印结果在不同 JDK 版本下锁标志位的呈现略有差异JDK 15 以后偏向锁默认关闭所有锁直接走轻量级看到101的机会会越来越少。4.3 为什么说偏向锁撤销最贵偏向锁撤销之所以比直接竞争轻量级锁还贵在于它需要safe point。JVM 要暂停所有 Java 线程才能安全地读取目标线程的执行状态、判断它是否还在同步块内。safe point 选择通常位于方法调用、循环回跳等位置如果持有线程正处于长循环JVM 可能一直在等待它进入 safe point这个等待时间不可控。更坑的是一次偏向锁撤销是有涟漪效应的JVM 需要遍历该类的所有实例更新它们的 epoch 字段或禁用标记这个 O(对象数量) 的操作在对象池很大的情况下会放大成本。所以我在项目里给过一条不太主流的建议对明确存在多线程竞争的共享对象启动参数直接加-XX:-UseBiasedLockingJDK 15 以前关掉偏向锁反而能减少撤销开销。前提是你的项目确实以多线程竞争为主——比如并发高的网关、缓存服务。反过来如果项目大量单线程访问共享对象偏向锁仍是友好的。这个取舍没有绝对正确只能根据实际压测数据来定不要听谁一句偏向锁没用就乱调参数。5. 面试高频追问与常见误区锁升级里的细节陷阱5.1 常见误区一偏向锁会自动释放前面讲过偏向锁只有发生竞争、触发撤销流程时才释放。持有线程正常退出同步块时不做任何写回操作锁标志位依然是101。这个不释放是有意为之——反正没竞争者标记保持着下次进入还能省掉 CAS。所以偏向锁有没有释放这个问题本身就要看语境单向看对于持有线程锁在其退出临界区后依然偏向它随时可再次进入这是释放了锁其他线程可以让偏向锁撤销后获取但对象头里线程 ID 没有被主动清空不是标准意义上释放后 Mark Word 变回无锁。5.2 常见误区二自旋一定发生在轻量级锁阶段自旋也会发生在重量级锁获取失败后的 Monitor enter 阶段——但这通常不是为了不挂起而是ObjectMonitor里有一个TrySpin阶段避免线程真的进入内核等待。实际上JDK 内置锁的自旋策略是一以贯之的能用户态解决就不进内核。不同点是轻量级锁阶段自旋是为了直接拿锁重量级锁阶段自旋是为了避免进入_EntryList挂起。二者触发时机、自旋上限、metadata 参照都不同面试别只答轻量级锁会自旋。5.3 常见误区三锁消除和锁粗化是两个多余的口头概念锁消除Lock Elimination是 JIT 编译器在逃逸分析Escape Analysis基础上做的优化如果 JVM 判断一个对象不会逃逸出当前线程那么对这个对象加锁是多余的直接削掉 synchronized 开销。典型例子是局部变量 synchronizedpublic String concat(String s1, String s2) { // 如果 sb 没有逃逸锁消除会把这里变成无锁操作 StringBuffer sb new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }这里的StringBuffer虽然内部方法都带 synchronized但对象只在方法内部使用不存在多线程竞争JIT 会在即时编译阶段把同步代码删除。这也是我反复跟团队说的一句话别在代码里自己发明单线程锁让 JIT 和锁升级机制去判断纯人肉判断往往过早优化。锁粗化Lock Coarsening则相反如果同一个对象的连续加锁/解锁操作之间没有其他线程介入的机会JIT 会把这些锁范围合并成一个更大的临界区减少加解锁次数。常见于循环里的锁操作。两个优化叠加解释了为什么 synchronized 在高性能代码里也没那么慢。5.4 常见误区四对象调用了 hashCode 就不走偏向锁这一条要分阶段看。如果 hashCode 是 Identity HashCode系统生成的且对象已经计算过了Mark Word 里已经存了 31 位 hashCode那线程 ID 放不进去偏向锁被禁用对象直接走轻量级锁。但如果你重写了hashCode()方法且 hash 值不是通过identityHashCode得到的JVM 未必把它塞进 Mark Word重写的 hash 值存的是数组/对象字段里偏向锁路径不受影响。实操中踩过这个坑一个高并发项目里用了大量 HashMap 的 key 对象这些对象运行时被调用hashCode()然后又被 synchronized ——结果发现所有锁直接是轻量级锁偏向锁根本没机会生效。排查方式就是用 JOL 看对象头发现 hashCode 字段占住了 Mark Word。5.5 关于 JDK 15 废弃偏向锁这个背景值得多说两句。JEP 374Deprecate Biased Locking给出的理由是偏向锁在现代硬件和 JVM 体系下收益下降因为现代 JVM 启动时间变短JIT 编译越来越早介入对象模型上的首次获取锁需要偏向收益窗口缩小。偏向锁在撤销时引入的安全点停顿在高并发服务里可能造成微服务可见的抖动。大量并行应用框架如 Java 21 的虚拟线程中线程数量激增偏向锁的线程 ID epoch 维护成本放大。JDK 18 开始偏向锁默认被禁用UseBiasedLocking默认 false。也就是说如果你用的是较新的 LTS 版本如 JDK 17、JDK 21就算写代码什么都不变锁升级的实际路径可能只有轻量级锁 → 重量级锁两段不会有偏向锁出现。网上老文章张口闭口偏向锁 → 轻量级锁 → 重量级锁三阶段现在要加上一个 JDK 版本的前缀条件不然面试官一句你确定当前 JDK 默认开启偏向锁吗就能把你问住。6. 从锁升级反推 Java 并发设计哲学理解了锁升级机制的每一个细节回头看 synchronized 的整体设计会发现一条清晰的主线让正确且高效的方案在不同的竞争烈度下自动切换而不是用一种万金油策略处理所有场景。这在工程上非常有借鉴意义。你自己写并发代码时也经常面临一套锁应对所有压力的尴尬临界区长时间无竞争你不想付出原子操作开销偶尔两三个线程偶遇你想要快速自旋高冲突时你又希望线程不燃烧 CPU、有序阻塞唤醒。Synchronized 用一条升级链把三种需求都覆盖了而且对应用层完全透明——你写一个synchronizedJVM 替你选择了当前最合适的实现。从这个角度再看ReentrantLock和synchronized之争就很容易理解怎么选了。ReentrantLock的优势集中在需要公平性、可中断、超时获取、多个 Condition 条件队列等高级特性上而synchronized的优势是语法简洁、JIT 优化空间大锁消除、锁粗化、随 JVM 版本自动演进。如果你的业务没有特殊需求用 synchronized 完全够如果需要精确控制等待策略或中断响应再考虑显式锁。6.1 参数调优清单基于实际项目整理聊太多原理容易飘最后列一张我整理过多次的参数清单适合真要做 JVM 锁行为调优的团队参考。参数作用默认值适用场景-XX:UseBiasedLocking开启偏向锁JDK 15 前 trueJDK 15 起 deprecatedJDK 18 起默认 false单线程/低竞争为主且 JDK 版本较低-XX:BiasedLockingStartupDelay偏向锁启动延迟毫秒4000调整偏向锁生效时机测试时可设为 0-XX:UseSpinning开启自旋true一般不动-XX:PreBlockSpin自旋次数上限10旧版本手工调整新版本自适应自旋后基本废弃-XX:EliminateLocks锁消除开关true一般不动依赖逃逸分析-XX:DoEscapeAnalysis逃逸分析开关true关闭会导致锁消除失效一般不动重要提醒调参前一定先用 JFR 或 arthas 确认瓶颈确实在锁上。我见过好几个团队花大力气调偏向锁参数最后定位到的问题其实是数据库连接池等待属于拿着锤子看什么都像钉子。6.2 锁升级与数据一致性之间的关系最后补一块很多教程不讲的锁升级机制如何间接保证数据一致性。看到热搜里有java怎么保证数据一致性这其实和 synchronized 的语义高度相关。synchronized 的可见性保证本质上依赖 JVM 内存屏障。偏向锁阶段虽然几乎零开销但它对共享变量的读写在临界区内的序依然受 Java 内存模型约束——监视器锁规则Monitor Lock Rule保证了同一个锁的 unlock 与后续 lock 之间存在 happens-before 关系。轻量级锁的 CAS 操作本身带全屏障语义重量级锁则通过操作系统的互斥锁天然保证内存可见性。锁升级改变的是实现代价没有改变内存语义。所以哪怕 JDK 18 之后偏向锁关闭了你的 synchronized 代码在数据一致性上绝不会因为锁升级路径变短而变弱——变的只是性能曲线。理解这一点对排查并发 bug 特别重要不要因为锁大多数时候没有竞争就觉得能放松对可见性的警惕跨线程读共享变量时该同步还是必须同步。6.3 一个老生常谈但值得重复的测试建议想真正吃透锁升级别只靠读文章一定要在你的项目里做一次基准测试。我给你一个最小验证结构public class LockUpgradeBenchmark { private static final int LOOP 1_000_000; private static long value; public static void main(String[] args) throws Exception { for (int i 0; i 5; i) { singleThreadBenchmark(第 (i 1) 轮); } } private static void singleThreadBenchmark(String label) { long start System.nanoTime(); for (int i 0; i LOOP; i) { synchronized (LockUpgradeBenchmark.class) { value; } } System.out.println(label 耗时: (System.nanoTime() - start) / 1_000_000 ms); } }把它分别跑在 JDK 8、JDK 11、JDK 17 上你会直观看到同一份代码在偏向锁启用和禁用状态下的耗时差。再用 JOL 打印对象头切换前后的 Mark Word 值比看十篇原理文章都管用。别嫌这个例子简单很多并发疑难问题的突破口恰恰是这种最朴素的可视化实验。锁升级不是一个可以靠死记硬背记住的知识点它是一个横跨 JVM 内存布局、指令集原子操作、操作系统线程调度的系统工程。把每个环节的为什么想透了面试、调优、排障都有底气。我写这篇文章的时候脑子里浮现的是自己在生产环境通过 JFR 看到 synchronized 高耗时然后一步步定位到偏向锁撤销抖动的那次经历——那种原来如此的感受希望你能在实际实验里也体验到一次。