Java 锁膨胀机制深度解析:从偏向锁到重量级锁的进化之路

Java 锁膨胀机制深度解析:从偏向锁到重量级锁的进化之路 Java 锁膨胀机制深度解析从偏向锁到重量级锁的进化之路在 Java 并发编程中synchronized关键字曾是性能低下的代名词。然而从 JDK 1.6 开始HotSpot 虚拟机对synchronized进行了大刀阔斧的优化引入了偏向锁Biased Locking、**轻量级锁Lightweight Locking和重量级锁Heavyweight Locking**的分级机制。这种机制的核心思想是“根据竞争程度动态调整锁的粒度”。在无竞争时尽量使用低成本操作在有竞争时再升级为高成本但功能强大的锁。这一过程被称为锁膨胀Lock Escalation。本文将深入剖析锁膨胀的全过程揭示 JVM 如何在不同阶段进行极致优化。一、基石对象头Object Header与 Mark Word要理解锁膨胀首先必须理解 Java 对象在内存中的布局。每个 Java 对象在堆内存中都包含一个对象头Object Header。在 64 位 JVM 开启压缩指针默认开启的情况下对象头通常包含两部分Mark Word标记字存储对象的运行时数据如哈希码、GC 分代年龄、锁状态标志、线程持有记录等。它是锁升级的关键载体。Klass Pointer类型指针指向方法区中类的元数据。Mark Word 的结构64 位压缩指针大小 (bit)内容说明25Hash Code对象哈希值未计算时为 04GC 分代年龄对象在新生代存活的次数1偏向锁标志0: 非偏向, 1: 偏向2锁标志00: 无锁, 01: 偏向锁, 10: 轻量级锁, 11: 重量级锁.........关键点锁的状态信息直接存储在对象头的 Mark Word 中。锁的升级过程本质上就是修改 Mark Word 内容的过程。注意从 JDK 15 开始偏向锁被标记为废弃DeprecatedJDK 18 中默认禁用。但在理解 JVM 历史演进和底层原理时偏向锁依然是不可或缺的一环。下文将以经典模型为主并在最后说明新版本的变化。二、第一阶段偏向锁Biased Locking—— “假设没有竞争”2.1 核心思想场景绝大多数锁在大部分时间内都是由同一个线程重复获取的例如单线程执行同步代码块或线程封闭的对象。优化策略如果一个线程获得了锁那么它就“偏向”于该线程。当该线程再次进入同步块时无需进行任何原子操作CAS只需检查 Mark Word 中记录的线程 ID 是否为自己即可。2.2 实现细节初始状态对象创建时Mark Word 的锁标志位为01偏向锁标志位为1可偏向但此时尚未绑定具体线程。首次获取线程 A 进入同步块。JVM 发现是可偏向锁通过CAS 操作将线程 A 的 ID 写入 Mark Word。CAS 成功线程 A 获得锁。重入线程 A 再次进入。JVM 检查 Mark Word 中的线程 ID发现就是自己。直接通过无任何额外开销。偏向撤销Revoke如果线程 B 试图获取该锁发生竞争。JVM 会暂停拥有锁的线程 ASafePoint检查线程 A 是否还在使用该锁。若线程 A 已退出或未使用直接将 Mark Word 重置为无锁状态或轻量级锁状态。若线程 A 仍在使用则触发偏向锁撤销升级为轻量级锁。性能收益在无竞争的单线程场景下偏向锁消除了所有同步原语CAS、互斥量的开销性能接近非同步代码。三、第二阶段轻量级锁Lightweight Locking—— “自旋避免阻塞”3.1 核心思想场景存在轻微的竞争但线程持有锁的时间很短。优化策略不使用操作系统层面的互斥量Mutex而是让用户态的线程通过**自旋Spinning**等待锁释放。因为线程切换用户态-内核态的开销远大于短时间自旋的 CPU 消耗。3.2 实现细节锁记录Lock Record轻量级锁依赖栈帧中的锁记录Displaced Mark Word。锁膨胀触发偏向锁被撤销或者对象一开始就不可偏向。线程尝试获取锁。压栈JVM 在当前线程的栈帧中分配一块空间称为锁记录。将对象头中的 Mark Word 复制一份到锁记录中称为Displaced Mark Word。CAS 替换线程尝试使用CAS 操作将对象头的 Mark Word 替换为指向当前栈帧中锁记录的指针。锁标志位变为00轻量级锁。结果判断成功线程获得锁执行同步代码。失败说明有其他线程竞争。自旋线程不会立即阻塞而是循环检测对象头的 Mark Word 是否恢复即锁是否被释放。自适应自旋JDK 1.6 引入了自适应自旋。自旋时间不再固定而是根据上一次在同一把锁上的自旋时间及锁拥有者的状态动态调整。如果上次自旋成功了这次就多转几圈如果失败了就少转几圈。升级阈值如果自旋超过一定次数默认 10 次可通过-XX:PreBlockSpin调整新版由 JVM 自适应决定仍未获取锁或者竞争线程数超过 1 个多于一人争抢锁将膨胀为重量级锁。性能收益避免了用户态到内核态的切换适合锁占用时间极短的场景。四、第三阶段重量级锁Heavyweight Locking—— “最后的防线”4.1 核心思想场景竞争激烈锁持有时间长自旋浪费大量 CPU 资源。优化策略使用操作系统底层的互斥量Mutex。线程获取不到锁时直接挂起阻塞进入等待队列让出 CPU 给其他线程。4.2 实现细节锁膨胀对象头的锁标志位变为10。Mark Word 中存储指向ObjectMonitorC 实现的监视器的指针。争夺过程线程尝试获取锁失败后被封装成ObjectWaiter节点放入EntryList等待队列。线程状态从RUNNABLE变为BLOCKED操作系统挂起该线程。当锁持有者释放锁时JVM 会从 EntryList 中唤醒一个线程或根据策略唤醒多个使其重新竞争。开销涉及用户态与内核态的切换上下文切换成本高。但保证了在高竞争下不会空转浪费 CPU。五、锁的降级不存在的这是一个常见的误区锁只能升级不能降级。升级路径无锁 - 偏向锁 - 轻量级锁 - 重量级锁。为何不降级如果在运行过程中将重量级锁降级为轻量级锁需要保证全局没有其他线程在竞争这在多线程环境下极难安全地判断和维护。为了简化实现并保证安全性一旦锁膨胀为重量级锁直到对象被垃圾回收之前它将一直保持重量级锁状态即使后续没有竞争了。六、新版本的变化偏向锁的退场虽然上述“三级锁”模型是经典的面试考点和原理基础但在实际生产环境的新版本 JDK 中情况有所变化JDK 15偏向锁被标记为Deprecated。JDK 18偏向锁默认禁用可以通过-XX:UseBiasedLocking强制开启但不推荐。原因现代应用架构变化现代应用多为多线程并发真正的“单线程重复获取锁”场景变少。撤销开销大一旦发生竞争偏向锁的撤销Revoke需要 Stop-The-WorldSTW暂停所有线程来扫描栈帧这个开销在某些高并发场景下反而比直接使用轻量级锁更大。简化逻辑移除偏向锁可以简化 JVM 内部复杂的锁状态机逻辑。现状在现代 JDK 中锁的升级路径通常简化为无锁 - 轻量级锁自旋 - 重量级锁。七、总结与启示JVM 的锁膨胀机制体现了计算机科学中经典的**“以空间换时间”和“分级处理”**思想锁状态适用场景核心优化手段代价偏向锁单线程重入记录线程 ID无 CAS无同步开销撤销时需 STW轻量级锁低竞争短时持有CAS 自旋避免内核切换占用栈空间自旋耗 CPU重量级锁高竞争长时持有操作系统 Mutex线程阻塞挂起用户态/内核态切换开销大给开发者的建议减少锁粒度尽量缩小同步代码块的范围缩短持锁时间让锁停留在轻量级阶段。避免过度优化不要手动强行指定锁类型信任 JVM 的动态调整能力除非你有极其特殊的性能分析数据。关注新版特性如果使用 JDK 15不必再纠结偏向锁的调优应更多关注CompletableFuture、StampedLock或ReentrantReadWriteLock等更高级的并发工具。排查死锁重量级锁一旦形成等待队列极易引发死锁务必使用jstack等工具定期监控。理解锁膨胀机制不仅是为了应对面试更是为了在编写高并发代码时心中有一幅清晰的“性能地图”知道每一行synchronized背后JVM 正在为你做什么。