这份合并后的深度分析涵盖了从底层存储到自动化流程,以及程序员如何通过代码逻辑“间接”干预锁命运的完整机制。
一、 锁状态的核心载体: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 在高版本中可能会弱化偏向锁,因此减少竞争和缩小临界区才是王道。