Fork me on GitHub
Hike News
Hike News

深度解析:Java 锁的膨胀机制(偏向锁、轻量级锁、重量级锁)

在 Android 的 ART(Android Runtime)和 HotSpot JVM 中,synchronized 关键字的性能优化主要依赖于锁的膨胀(Inflation)机制。系统会根据竞争情况,自动在四种状态间转换:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁


一、 锁状态的核心存储:Mark Word

锁的信息存储在 Java 对象的对象头(Object Header)中的 Mark Word 字段里。

  • Mark Word 会根据锁状态的变化,复用自己的空间来存储:偏向线程 ID、指向栈中锁记录的指针、或指向互斥量(Monitor)的指针。

二、 四种锁状态详解

1. 偏向锁 (Biased Locking)

  • 核心思想:假设锁总是由同一个线程多次获得,那么就没有必要进行同步操作(如 CAS)。
  • 实现:在 Mark Word 中记录当前线程的 ID。下次该线程进入时,只需检查 ID 是否一致,完全不需要 CPU 原子指令。
  • 优缺点
    • 优点:单线程执行临界区代码时,性能接近无锁。
    • 缺点:一旦出现另一个线程竞争,撤销偏向锁会产生较大的性能开销(需要等待全局安全点 Safe Point)。
  • Android 现状:在 Android ART 中,偏向锁的实现逻辑与 HotSpot 略有不同,但在高版本中出于简化系统和减少 Safe Point 停顿的考虑,偏向锁的使用逐渐被弱化甚至在某些场景下默认关闭。

2. 轻量级锁 (Lightweight Locking)

  • 触发时机:当偏向锁被撤销,或者有第二个线程尝试获取锁(但竞争不激烈)时。
  • 实现:线程在自己的虚拟机栈中创建一个名为“锁记录(Lock Record)”的空间,尝试用 CAS 操作将对象的 Mark Word 指向这个记录。
  • 自旋优化:如果 CAS 失败,线程不会立即阻塞,而是执行一段自旋(Spinning)循环,期望持锁线程能迅速释放。
  • 优点:避免了操作系统内核态与用户态切换的昂贵开销。

3. 重量级锁 (Heavyweight Locking)

  • 触发时机:竞争激烈。如果自旋超过一定次数,或者在轻量级锁状态下又有第三个线程来抢锁。
  • 实现:Mark Word 指向堆中的 Monitor(监视器) 对象。Monitor 依赖于操作系统的 mutex 指令。
  • 行为:未能抢到锁的线程会被挂起(Park),进入阻塞状态,等待操作系统唤醒。
  • 代价:涉及内核态切换,上下文切换开销巨大。

三、 锁的升级(膨胀)流程图

  1. 初始状态:对象处于“无锁”或“可偏向”状态。
  2. 偏向开启:线程 A 访问,Mark Word 记录 A 的 ID。
  3. 轻度竞争:线程 B 访问,发现 ID 不匹配。撤销偏向锁,升级为轻量级锁。
  4. CAS 竞争:线程 A 和 B 通过 CAS 争夺锁记录指针。
  5. 激烈竞争:CAS 失败次数过多,轻量级锁膨胀为重量级锁,线程 B 进入阻塞队列。

注意:锁的升级通常是不可逆的(但在某些 JVM 实现中,在全局垃圾回收 GC 时可能会发生锁降级)。


四、 补充优化:锁消除与锁粗化

除了状态膨胀,编译器(JIT)还做了以下两项神级优化:

1. 锁消除 (Lock Elimination)

  • 原理:通过逃逸分析,如果发现一个对象只会在当前线程内部使用(不会被其他线程看到),JIT 编译器会直接把 synchronized 去掉。
  • 例子:在方法内部定义的 StringBuffer.append()

2. 锁粗化 (Lock Coarsening)

  • 原理:如果一系列连续的操作都对同一个对象加锁(如循环内加锁),编译器会把加锁的范围扩大到整个操作序列外部,避免频繁开关锁。

🎙 面试考察点:资深级别回答

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

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

Q:在 Android 开发中,这套机制给我们的启示是什么?

  • :1. 尽量减少锁的竞争范围(减小临界区)。2. 对于高频调用的代码,优先使用无锁结构(CAS/Atomic),因为一旦膨胀到重量级锁,在移动端有限的算力下,性能损耗非常直观。