Java 锁的膨胀机制(偏向锁、轻量级锁、重量级锁)

这份合并后的深度分析涵盖了从底层存储到自动化流程,以及程序员如何通过代码逻辑“间接”干预锁命运的完整机制。


一、 锁状态的核心载体:Mark Word

在 Java(包括 Android ART 和 HotSpot JVM)中,synchronized 的性能优化主要依赖于膨胀(Inflation)机制。锁的状态信息存储在对象头(Object Header)的 Mark Word 字段中:

  • 空间复用:Mark Word 会根据锁状态的变化,动态存储偏向线程 ID、指向栈中锁记录的指针、或指向互斥量(Monitor)的指针。
  • 四个阶段:锁会自动在 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 这四种状态间转换。

二、 四种锁状态详解:从“极致偏爱”到“失控挂起”

1. 无锁 (Unlocked)

对象的初始状态,Mark Word 中没有线程竞争的相关信息。

2. 偏向锁 (Biased Locking)

  • 核心思想:假设锁总是由同一个线程多次获得,从而消除同步操作(如 CAS)的开销。
  • 实现:JVM 在 Mark Word 中记录当前线程 ID。该线程再次进入时,只需检查 ID 是否一致,性能接近无锁。
  • 潜规则:JVM 启动初期通常有约 4s 的延迟开启时间,以避开启动阶段大量类加载导致的线程竞争。

3. 轻量级锁 (Lightweight Locking)

  • 触发时机:当有第二个线程尝试获取锁,但竞争并不激烈时。
  • 实现:线程在自己的虚拟机栈中创建“锁记录(Lock Record)”,尝试通过 CAS 将 Mark Word 指向该记录。
  • 适应性自旋:抢不到锁的线程会在门口“打转”(自旋)。JVM 会根据历史成功率决定自旋时长,避免立即挂起线程。

4. 重量级锁 (Heavyweight Locking)

  • 触发时机:竞争激烈。当自旋次数过多,或又有第三个线程加入抢锁。
  • 实现:Mark Word 指向堆中的 Monitor(监视器),依赖操作系统的 mutex 指令。
  • 代价:未能抢到锁的线程会被挂起(Park),涉及内核态与用户态的上下文切换,开销巨大。

三、 程序员如何“间接”控制锁的命运?

虽然锁膨胀是 JVM 的“自动挡”,但你的逻辑决定了系统走哪条优化路径:

优化维度 做法与建议 对 JVM 的影响
锁的粒度 尽量使用 synchronized(lockObj) 缩小同步块。 减少虚假竞争,让锁尽可能维持在轻量级状态。
持锁时长 同步块内只做核心状态变更,严禁长时间 IO。 防止自旋线程等不及而强行触发重量级锁膨胀。
逃逸分析 利用局部变量加锁(对象不逃逸出方法)。 JIT 编译器会触发锁消除,运行时完全不加锁。
锁粗化 避免在循环体内高频加锁。 编译器会将多次加锁合并,减少开关锁的性能损耗。

🎙 面试官进阶 Q&A

Q:既然重量级锁慢,为什么不一直用轻量级锁自旋?

  • :自旋会消耗 CPU 算力(CPU 在跑空转指令)。如果持锁线程执行时间很长,一直自旋会白白浪费 CPU 资源。因此,达到一定阈值后必须膨胀为重量级锁,让竞争线程去“睡觉”(挂起),把资源让给真正干活的线程。

Q:在 Android 开发中,这套机制有什么实践意义?

  • :1. 对于高频调用的代码,优先考虑 Atomic 原子类或无锁结构,避免一旦膨胀到重量级锁后,移动端有限的算力导致明显的卡顿。2. 意识到 Android ART 在高版本中可能会弱化偏向锁,因此减少竞争和缩小临界区才是王道。
,