目录
  1. 1. 一、synchronized 的字节码表示
    1. 1.1. 1.1 同步代码块:monitorenter / monitorexit
    2. 1.2. 1.2 同步方法:ACC_SYNCHRONIZED 标志
  2. 2. 二、Java 对象头与 Mark Word
    1. 2.1. 2.1 ART 中的 Object 类定义
    2. 2.2. 2.2 32 位 Mark Word 的位布局
    3. 2.3. 2.3 64 位 Mark Word 的位布局
  3. 3. 三、锁升级(Lock Escalation)的完整路径
    1. 3.1. 3.1 状态机概览
    2. 3.2. 3.2 偏向锁(Biased Lock)
    3. 3.3. 3.3 轻量级锁(Thin Lock / Lightweight Lock)
    4. 3.4. 3.4 重量级锁(Heavyweight Lock / Fat Lock)
  4. 4. 四、Object.wait / notify 与 futex
    1. 4.1. 4.1 wait/notify 的 Monitor 实现
    2. 4.2. 4.2 Wait Morphing 优化
    3. 4.3. 4.3 futex 原理
  5. 5. 五、重入性(Reentrancy)与锁计数
  6. 6. 六、锁优化技术
    1. 6.1. 6.1 锁粗化(Lock Coarsening)
    2. 6.2. 6.2 锁消除(Lock Elision)
    3. 6.3. 6.3 锁降级(Lock Deflation)
    4. 6.4. 6.4 偏向锁在 Android ART 中的特殊处理
  7. 7. 七、死锁检测机制
  8. 8. 八、AOSP 关键源码路径总结
  9. 9. 九、ART 解释器中的 monitorenter/monitorexit 实现
    1. 9.1. 9.1 monitorenter 的解释器路径
    2. 9.2. 9.2 monitorexit 的解释器路径
    3. 9.3. 9.3 嵌套 monitorenter/monitorexit 的 Lock Record 栈
  10. 10. 十、安全点与线程挂起机制
    1. 10.1. 10.1 安全点(Safepoint)的工作原理
    2. 10.2. 10.2 安全点对 Android 应用的影响
  11. 11. 十一、LockWord 状态转换实战
    1. 11.1. 11.1 状态转换序列
    2. 11.2. 11.2 实战辨识技巧
  12. 12. 十二、synchronized vs JUC 锁 性能对比
    1. 12.1. 12.1 不同场景下的性能特征
    2. 12.2. 12.2 选择建议
  13. 13. 十三、常见锁问题的调试方法
    1. 13.1. 13.1 死锁检测:通过线程 Dump
    2. 13.2. 13.2 通过 ANR trace 分析锁争用
    3. 13.3. 13.3 Android Studio Profiler 的锁监控
  14. 14. 十四、AOSP 关键源码路径总结(续)
  15. 15. 十五、深入 futex:从 Java 到 Linux 内核的完整路径
    1. 15.1. 15.1 Wait 路径(Thread.wait → FUTEX_WAIT)
    2. 15.2. 15.2 Wake 路径(Thread.notify → FUTEX_WAKE)
    3. 15.3. 15.3 性能数据(近似值,x86-64 Linux)
  16. 16. 面试问答
【深入理解JVM字节码】第五篇、synchronized实现原理

一、synchronized 的字节码表示

Java 中 synchronized 有两种使用形式:同步方法和同步代码块。它们在字节码层面的表示截然不同。

1.1 同步代码块:monitorenter / monitorexit

同步代码块编译后会生成 monitorentermonitorexit 指令对,以及负责异常安全性的异常表。以如下代码为例:

public void syncBlock() {
synchronized (this) {
doSomething();
}
}

使用 javap -c -v 反编译得到:

public void syncBlock();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: invokevirtual #2 // Method doSomething:()V
8: aload_1
9: monitorexit
10: goto 18
13: astore_2
14: aload_1
15: monitorexit
16: aload_2
17: athrow
18: return
Exception table:
from to target type
4 8 13 any
13 16 13 any

这里有三个关键细节:

首先,synchronized 对象引用必须被保存到局部变量中(偏移 0-2:aload_0 + dup + astore_1),因为 monitorenter 会消费栈顶引用,而 monitorexit 需要再次从局部变量表加载同一个引用。如果 synchronized 对象是一个临时表达式(而非事先存储的变量),编译器也会生成 dup+astore 来保留一份副本。

其次,编译器必须生成一个隐式的 catch-all 异常处理器(Exception table 中 from 4 to 8 target 13 type any)。如果同步块内部抛出异常,程序跳转到偏移 13,先执行 monitorexit 释放锁(偏移 15),再通过 athrow(偏移 17)重新抛出异常。这确保了异常场景下锁必然被释放——这是 synchronized 相对于手动 Lock.lock()/unlock() 的核心安全保障之一。

第三,异常处理路径中还有一个嵌套的异常处理器(from 13 to 16 target 13 type any),用于处理 monitorexit 自身可能抛出的异常(虽然理论上 monitorexit 不应抛出异常,但如果对象头损坏或 monitor 状态异常,JVM 需要处理此边缘情况)。

字节码布局总结(同步代码块):

[保存锁对象引用到局部变量]
monitorenter ← 获取锁
[同步块代码体]
[加载锁对象引用]
monitorexit ← 正常释放锁
goto [end]
[异常处理入口点]
[加载锁对象引用]
monitorexit ← 异常时释放锁
athrow ← 重新抛出异常
[end]

1.2 同步方法:ACC_SYNCHRONIZED 标志

同步方法不需要 monitorenter/monitorexit 指令,而是在方法表的 access_flags 中设置 ACC_SYNCHRONIZED 标志(0x0020)。对于实例方法,锁对象是 this;对于静态方法,锁对象是该方法所属的 Class 对象。

public synchronized void syncMethod() {
doSomething();
}

字节码:

public synchronized void syncMethod();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED // ← 注意
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method doSomething:()V
4: return

JVM 在执行带有 ACC_SYNCHRONIZED 标志的方法时,自动在方法入口获取锁、在方法出口(正常返回或异常抛出)释放锁。对于实例方法,JVM 在执行第一条指令前隐式执行 monitorenter(this),在 return 指令后或异常路径中隐式执行 monitorexit(this)。这种”声明式”的同步方式比代码块方式更简洁,因为异常释放由 JVM 保证,字节码中不需要显式的异常表。

代码块 vs 方法的关键差异总结:

特性 同步代码块 同步方法
字节码表示 monitorenter + monitorexit + 异常表 ACC_SYNCHRONIZED 标志
锁对象 任意对象(由代码指定) 实例方法:this;静态方法:Class 对象
异常安全 通过异常表显式保证 JVM 隐式保证
灵活性 高(可缩小同步范围,选择锁对象) 低(整个方法被锁定)
字节码体积 较大(额外 ~15+ 字节指令和异常表) 几乎无额外体积

二、Java 对象头与 Mark Word

理解 synchronized 的底层实现,必须先理解 Java 对象的内部结构。在 HotSpot JVM 和 ART 中,每个 Java 对象都有一个对象头(object header)。

2.1 ART 中的 Object 类定义

在 AOSP 的 art/runtime/mirror/object.h 中,Object 类的核心数据结构如下:

// art/runtime/mirror/object.h
class MANAGED Object {
private:
// 对象头:包含锁状态、GC 状态、hash 等信息
HeapReference<Class> klass_; // 指向类的指针(32位系统4字节,64位系统8字节)
uint32_t monitor_; // 锁字 / Lock Word(32位)

// 实例字段从 offset 8 (32位) 或 offset 12 (64位) 开始
// ...
};

在 32 位 ART 中,对象头共 8 字节(4 字节 klass_ + 4 字节 monitor_)。在 64 位 ART 中,如果开启了压缩指针(compressed oops),对象头可以是 12 字节(8 字节 Mark Word + 4 字节压缩 klass 指针);如果未压缩,则对象头为 16 字节(8 字节 Mark Word + 8 字节 klass 指针)。

2.2 32 位 Mark Word 的位布局

monitor_ 字段(也称为 Lock Word 或 Mark Word)是一个多用途字段,根据低位的锁标志解释为不同含义。以 32 位 HotSpot JVM 为例(ART 的逻辑相同但具体位布局可能略有差异):

锁状态 位[31:3] 位[2:0] 含义
无锁(unlocked) hash_code (25位) + age (4位) 001 存储 identity hash code 和 GC 分代年龄
偏向锁(biased) thread_id (23位) + epoch (2位) + age (4位) 101 记录持有偏向锁的线程 ID
轻量级锁(thin) lock_record_ptr (30位) 000 指向当前线程栈中 Lock Record 的指针
重量级锁(fat) monitor_ptr (30位) 010 指向 ObjectMonitor 对象的指针
GC 标记(marked) forwarding_ptr (30位) 011 GC 期间的转发指针

2.3 64 位 Mark Word 的位布局

在 64 位 JVM 中(包括 ART 的 64 位模式),Mark Word 有 64 位,布局如下:

锁状态 位布局 锁位(biased_lock:lock)
无锁(unlocked) unused:25 | hash:31 | unused:1 | age:4 | biased_lock:1 | lock:2 0:01
偏向锁(biased) thread_id:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 1:01
轻量级锁(thin) lock_record_ptr:62 | lock:2 00
重量级锁(fat) monitor_ptr:62 | lock:2 10
GC 标记(marked) forwarding_ptr:62 | lock:2 11

解读要点:

  • biased_lock = 1, lock = 01:偏向锁模式。高 54 位存储持有偏向的线程 ID。
  • biased_lock = 0, lock = 01:无锁状态。31 位存储 identity hash code。
  • lock = 00:轻量级锁。62 位存储指向当前线程栈帧中 Lock Record 的指针。
  • lock = 10:重量级锁。62 位存储指向 ObjectMonitor 的指针。
  • lock = 11:GC 标记,62 位存储 GC 转发指针。

identity hash code 的惰性计算:当一个对象的 hashCode() 方法首次被调用(且类未重写 hashCode()),JVM 生成一个随机 hash 并存入 Mark Word 的 hash 区域。一旦 hash 被计算并写入,偏向锁就无法再被安装到该对象上(因为 hash 区域被占用了),这个对象直接走轻量级锁或重量级锁路径。这是系统中有些对象永远拿不到偏向锁的一个常见原因。

三、锁升级(Lock Escalation)的完整路径

synchronized 在 JVM/ART 中的实现经历了从”全部重量级”到”偏向锁→轻量级锁→重量级锁”的演化。Java 6 引入的锁升级机制是 synchronized 性能优化的里程碑。

3.1 状态机概览

┌──────────────────────┐
│ 无锁 (unlocked) │
│ lock bits = 001 │
└──────┬───────────────┘
│ 第一个线程获取

┌──────────────────────┐
│ 偏向锁 (biased) │
│ lock bits = 101 │
└──────┬───────────────┘
│ 另一线程竞争

┌──────────────────────┐
│ 轻量级锁 (thin) │
│ lock bits = 000 │
└──────┬───────────────┘
│ 自旋超时 / 第三个线程 / wait()

┌──────────────────────┐
│ 重量级锁 (fat) │
│ lock bits = 010 │
└──────────────────────┘
(不会自动降级)

核心特征:锁只能升级,不能降级。一旦膨胀为重量级锁,即使竞争消失,锁也不会回退为轻量级锁或偏向锁。降级只在极特殊的情况下发生(如 HotSpot 的 safepoint 批量降级或 ART MonitorPool 压缩时)。

3.2 偏向锁(Biased Lock)

设计目标:解决大多数锁实际上只有一个线程反复获取的场景(如 Vector 和 Hashtable 的内部方法,线程池中的重复操作)。

偏向锁的设计哲学是:如果一个锁至今只被一个线程获取过,那么该线程再次获取时不需要任何同步操作,直接验证 Mark Word 中的线程 ID 是否匹配即可。

获取过程(快速路径):

  1. 检查 Mark Word 的低 3 位是否为 101(偏向模式),且 Thread ID 为 0(未偏向)或等于当前线程 ID。
  2. 如果 Thread ID 匹配当前线程:直接成功——这是最快路径,没有任何 CAS 或内存屏障操作。仅需一次读 Mark Word。
  3. 如果 Thread ID 为 0(未偏向任何线程):执行一次 CAS 将当前线程 ID 写入 Mark Word。CAS 成功则获取偏向锁。
  4. 如果 CAS 失败(说明发生了竞争):在安全点(safe point)撤销偏向锁,升级为轻量级锁。

撤销(Revocation)的代价:偏向锁的撤销需要等到全局安全点(GC safepoint,所有线程都进入可以被检查的状态)。在安全点中,JVM 暂停持有偏向锁的线程,检查该线程是否还在同步块中:

  • 如果持有线程已退出同步块:直接将 Mark Word 恢复为无锁状态。
  • 如果持有线程仍在同步块内:将偏向锁升级为轻量级锁(为该线程在栈上分配 Lock Record)。

撤销操作的高昂代价(需要全局 STW 暂停)是偏向锁的最大缺点。当系统中同一把锁频繁在不同线程间切换时,偏向锁反而成为性能负担。

批量重偏向(Bulk Rebias):当一个类的多个实例频繁遇到偏向锁撤销时,JVM 不再逐个撤销,而是执行批量重偏向——在安全点中将属于该类的所有对象的偏向线程 ID 更新为新的主导线程。这大大减少了安全点的频率。

批量撤销(Bulk Revocation):当批量重偏向的执行次数超过阈值(默认 JVM 参数 BiasedLockingBulkRevokeThreshold=40,即某个类的撤销次数达到 40 次),JVM 将该类标记为”不可偏向”,该类后续创建的所有对象都不会再尝试偏向锁,直接走轻量级锁路径。

偏向启动延迟(BiasedLockingStartupDelay):HotSpot 在 JVM 启动后的前 4 秒(默认值)不会启用偏向锁。这是因为启动阶段有大量类加载和初始化的同步操作,如果启用偏向锁会导致大量不必要的撤销。这个延迟可以通过 -XX:BiasedLockingStartupDelay=0 关闭。

Android ART 中的偏向锁:ART 对偏向锁的支持与 HotSpot 有所不同。Android O (8.0) 之前,ART 默认不启用偏向锁。Android O 之后,部分引入了偏向锁机制,但由于移动端 CPU 核数较少(4-8 核),撤销偏向锁需要的安全点暂停对 UI 线程的影响更为显著,所以 ART 的偏向锁实现比 HotSpot 更为保守。相关代码位于 art/runtime/monitor.cc

3.3 轻量级锁(Thin Lock / Lightweight Lock)

当一个线程尝试获取已被其他线程偏向的锁,且该锁的偏向被撤销后仍有竞争时,锁升级为轻量级锁。轻量级锁通过在线程栈上分配锁记录(Lock Record)实现,依赖 CAS 自旋。

获取过程:

  1. 在线程栈帧上分配一个 Lock Record 空间(即 BasicLock 对象)。
  2. 将对象的 Mark Word 拷贝到 Lock Record 的 displaced_header 字段中(保留原始的 hash code、分代年龄等信息)。
  3. 通过 CAS 操作,将对象的 Mark Word 替换为指向该 Lock Record 的指针(指针的低 2 位为 00,即 lock = 00)。
  4. 如果 CAS 成功:轻量级锁获取成功。
  5. 如果 CAS 失败:说明发生竞争。进入自适应自旋或直接膨胀为重量级锁。

释放过程:

  1. 将 Lock Record 中的 displaced_header 通过 CAS 写回对象的 Mark Word。
  2. 如果 CAS 成功:锁释放成功,Mark Word 恢复到加锁前的状态。
  3. 如果 CAS 失败:说明在持有期间有其他线程等待过,锁已膨胀为重量级锁。此时需要走重量级释放路径(唤醒等待线程)。

可重入的轻量级锁:当同一线程重入获取轻量级锁时(例如嵌套 synchronized 块),ART 不是再次分配 Lock Record 并 CAS Mark Word,而是在新的 Lock Record 中将 displaced_header 设为 0(表示”重入”)。释放时检查 displaced_header 是否为 0:为 0 则仅弹出 Lock Record(减少嵌套计数);非 0 则执行 CAS 写回。

自适应自旋(Adaptive Spinning):CAS 失败的线程不会立即阻塞,而是在一定次数的忙循环中反复尝试(自旋)。自旋次数不是固定的:

  • 如果在同一个锁上近期的自旋成功了,增加自旋次数(因为这次很可能也会成功)。
  • 如果近期自旋总失败,减少自旋次数甚至不自旋直接阻塞。
  • JVM 维护每个锁的自旋成功历史,基于指数加权移动平均做自适应决策。

自旋的边界条件:

  • 锁的持有者在另一个 CPU 核心上运行时,自旋才有意义(持有者可能很快释放锁)。
  • 如果持有者在同一个核心上(或者当前是单核系统),自旋是浪费——因为只有当前线程被挂起后,持有者才有机会执行。

在 ART 的源码 art/runtime/monitor.cc 中,Monitor::MonitorLock 函数在获取重量级锁时也包含了自旋逻辑的尝试。

3.4 重量级锁(Heavyweight Lock / Fat Lock)

当自旋等待超时,或有第三个以上线程参与竞争,或锁的持有者调用了 wait(),锁膨胀为重量级锁。重量级锁通过操作系统的互斥机制实现,在 Linux 上是 futex(Fast Userspace muTEX)。

膨胀过程(在 ART 的 Monitor::InflateThinLocked 中):

// art/runtime/monitor.cc(简化逻辑)
void Monitor::InflateThinLocked(Thread* self, Handle<mirror::Object> obj,
LockWord lock_word) {
// 1. 从 MonitorPool 中分配一个 Monitor 对象
Monitor* monitor = MonitorPool::CreateMonitor(self);

// 2. 初始化 Monitor 的 owner_、lock_count_ 等字段
monitor->owner_ = thin_owner; // 从 thin lock 中提取当前持有者
monitor->lock_count_ = 1; // 重入计数为 1

// 3. CAS 将对象头中的 LockWord 替换为指向 Monitor 的指针
LockWord new_lock_word = LockWord::FromMonitorId(monitor->monitor_id_);
// CAS 替换:如果 Mark Word 还是原来的 thin lock 状态,则替换为 fat
// 如果 Mark Word 已经变了(说明别的线程已经 inflate 了),则竞争失败,使用别人的 Monitor
}

Monitor 对象(即 ObjectMonitor,在 art/runtime/monitor.h 中定义)包含以下核心字段:

class Monitor {
Thread* owner_; // 锁的持有者线程,nullptr 表示无锁
uint32_t lock_count_; // 重入计数(支持同一线程多次进入同步块)
monitor_id_t monitor_id_; // 在 MonitorList 中的唯一 ID
Mutex monitor_lock_; // 保护 wait_set_ 和内部状态的互斥锁
ConditionVariable monitor_cont_; // 基于 futex 的条件变量
Thread* wait_set_; // 等待线程链表(调用了 wait() 的线程)
Thread* entry_list_; // 等待获取锁的线程链表(EntryList)
Thread* cxq_; // 竞争队列(contention queue)
};

Monitor 的队列模型:

当线程尝试获取已被持有的重量级锁时,它被加入到竞争队列(cxq_)中。当锁被释放时,持有者线程从 cxq_entry_list_ 中取出一个线程并唤醒。这个”两阶段队列”设计(cxq 收集新请求,entry_list 是就绪等待队列)减少了锁释放时的队列操作竞争。

重量级锁的获取/释放通过 Monitor 对象的 Lock / Unlock 方法实现,其底层依赖 Linux 的 futex 系统调用。

四、Object.wait / notify 与 futex

4.1 wait/notify 的 Monitor 实现

Object.wait()Object.notify()Object.notifyAll() 必须在 synchronized 块内部调用,它们在字节码层面没有特殊指令,而是作为普通的 JNI 本地方法实现。

在 ART 中,这三个方法注册在 Object 类中,实际实现在 art/runtime/monitor.cc

Monitor::Wait

  1. 确保当前线程持有该对象的 Monitor(owner_ == self,否则抛出 IllegalMonitorStateException)。
  2. 释放 Monitor(通过 Monitor::Unlock 递减 lock_count_ 直到为 0,然后唤醒队列中的下一个等待者)。
  3. 将当前线程加入 Monitor 的 wait_set_ 链表。
  4. 在条件变量 monitor_cont_ 上等待(Wait → 底层 futex(FUTEX_WAIT))。
  5. 被唤醒后,重新获取 Monitor(Monitor::Lock),恢复 lock_count_

Monitor::Notify

  1. wait_set_ 中取出一个线程(通常是从头部)。
  2. 通过 monitor_cont_.Signal 唤醒该线程(底层 futex(FUTEX_WAKE, 1))。
  3. 被唤醒的线程不会立即执行——它需要先重新获取 Monitor(此时它进入 cxq_entry_list_ 队列)。

Monitor::NotifyAll

  1. 遍历整个 wait_set_,将每个线程移出并准备好被调度。
  2. 通过 monitor_cont_.Broadcast 唤醒所有等待线程(futex(FUTEX_WAKE, INT_MAX))。

4.2 Wait Morphing 优化

HotSpot JVM 实现了一种称为 “wait morphing” 的优化:当 notify 被调用后,被唤醒的线程不会立即被放入竞争队列,而是保持在一个过渡状态。如果通知线程在退出同步块之前又调用了 notifyAll,这一批线程可以批量地转移到 entry_list,减少了多次 futex 系统调用的开销。

4.3 futex 原理

futex 是 Linux 内核提供的快速用户态锁机制,不需要为无竞争情况支付系统调用的开销。futex 将一个 32 位整数作为”锁变量”放在用户空间,线程通过原子操作修改锁变量的值,只有需要阻塞/唤醒等待时才进入内核:

  • **futex(uaddr, FUTEX_WAIT, val, ...)**:如果 *uaddr == val,则将当前线程挂起等待。比较和挂起是原子的(由内核保证,避免了 TOCTOU 竞态)。
  • **futex(uaddr, FUTEX_WAKE, n, ...)**:唤醒最多 n 个在 uaddr 上等待的线程。

ART 中的 ConditionVariable 类(art/runtime/base/mutex.h)封装了 futex 调用。Monitor::monitor_cont_ 是一个 ConditionVariable 实例,它的 WaitSignalBroadcast 最终对应 futex(FUTEX_WAIT)futex(FUTEX_WAKE)

futex 的关键性能特征:

  • 无竞争时,锁获取只需一次原子 CAS(用户态操作,~10-20 ns)。
  • 有竞争且需要阻塞时,需要一次 futex(FUTEX_WAIT) 系统调用(~500-1000 ns)。
  • 唤醒线程时的 futex(FUTEX_WAKE) 也是一个系统调用。

这才是 synchronized “轻量级”的本质——大多数场景下锁获取在用户态即可完成,仅在真正需要挂起/唤醒时才进入内核。

五、重入性(Reentrancy)与锁计数

synchronized 是可重入锁,即同一个线程可以多次获取同一把锁而不会死锁。实现方式是通过重入计数:

重量级锁(Monitor): 使用 lock_count_ 字段计数。

  • 首次获取lock_count_ = 1owner_ = current_thread
  • 重入:检查 owner_ == current_thread,则 lock_count_ += 1,无需任何 CAS 或 futex 操作。
  • 退出lock_count_ -= 1,仅当 lock_count_ == 0 时将 owner_ = nullptr 并真正释放锁。

轻量级锁(Thin Lock): 使用 displaced_header == 0 标记重入。

  • 首次获取:Lock Record 的 displaced_header 存储原始的 Mark Word。
  • 重入:新 Lock Record 的 displaced_header = 0
  • 退出:如果 displaced_header != 0,CAS 将其写回 Mark Word(释放);如果 == 0,仅弹出 Lock Record(减少嵌套深度)。

在字节码层面,嵌套的同步块会产生嵌套的 monitorenter/monitorexit 对,JVM 在运行时通过上述计数机制跟踪嵌套深度。例如:

synchronized (obj) {          // lock_count_ = 1
synchronized (obj) { // lock_count_ = 2 (重入,快速路径)
// ...
} // lock_count_ = 1
} // lock_count_ = 0 (释放)

在 ART 的解释器中(art/runtime/interpreter/interpreter_common.cc),OP_MONITOR_ENTER 处理中调用了 Monitor::MonitorEnter,该函数在执行 Monitor::Lock 前首先检查 owner_ == self,如果是则仅递增 lock_count_,完全避免了重量级锁路径。这确保了重入场景下的最优性能。

六、锁优化技术

6.1 锁粗化(Lock Coarsening)

当编译器(JIT 或 AOT)检测到多个连续的 synchronized 块操作同一个锁对象时,会将这些小块合并为一个更大的同步块,减少 monitorenter/monitorexit 的次数。

示例(优化前):

public void append(String a, String b) {
synchronized (this) { sb.append(a); } // monitorenter/monitorexit 对 1
// 其他非同步操作...
synchronized (this) { sb.append(b); } // monitorenter/monitorexit 对 2
}

优化后(JIT 内部表示):

public void append(String a, String b) {
synchronized (this) { // 仅一次 monitorenter
sb.append(a);
// 其他非同步操作...(被拉入同步块,可能增加锁持有时间但有逃逸分析保证线程安全)
sb.append(b);
} // 仅一次 monitorexit
}

锁粗化的权衡:减少锁操作次数(降低 CAS/futex 开销),但可能增大锁持有区间(增加竞争概率)。JIT 编译器基于 profiling 数据决定是否执行粗化——如果该锁的竞争很低,粗化通常是净收益。

6.2 锁消除(Lock Elision)

当逃逸分析(Escape Analysis)确认一个对象只会被当前线程访问(不逃逸到其他线程),编译器可以直接消除对该对象的 synchronized 操作——因为不存在竞争的可能性。

典型场景: StringBuffer 在单线程中的使用:

public String concat() {
StringBuffer sb = new StringBuffer(); // sb 不逃逸
sb.append("a");
sb.append("b");
return sb.toString();
}

StringBuffer.append()synchronized 方法,但 JIT 通过逃逸分析确定 sb 对象不会离开当前栈帧,因此完全消除append 的锁操作。锁消除后,StringBuffer 的性能与 StringBuilder 相当。

必要条件:

  • 同步对象必须是局部变量(或方法参数且在调用链中不逃逸)。
  • 逃逸分析必须能够证明该对象不暴露给任何其他线程(不通过静态字段、返回、参数传递等方式逃逸)。

锁消除在 ART 的 optimizing compiler 中实现,相关代码在 art/compiler/optimizing/ 的逃逸分析和优化 pass 中。

6.3 锁降级(Lock Deflation)

虽然正常运行时锁不会降级,但 HotSpot 和 ART 都在特定条件下实现了某种形式的”降级”:

HotSpot:在 safepoint 期间(如 GC),JVM 遍历所有 Monitor 对象。如果发现某个 Monitor 没有被任何线程持有且等待队列为空,就将其回收并将对应对象的 Mark Word 恢复为无锁状态。这个过程称为 “monitor deflation”。

ARTMonitorPoolart/runtime/monitor_pool.cc)维护一个全局 Monitor 池。当 Monitor 不再被任何对象引用时,它被回收到池中供后续复用。在 MonitorPool 空间紧张时,ART 会执行 “monitor deflation”,释放不再被引用的 Monitor。这与 HotSpot 的降级不同——ART 的回收是针对 Monitor 对象的池化复用,而非真正的运行时锁状态降级。

6.4 偏向锁在 Android ART 中的特殊处理

Android ART 对偏向锁的处理与 HotSpot 有所不同。在 ART 的源代码 art/runtime/monitor.cc 中,偏向锁的实现称为 “biased locking”,轻量级锁称为 “thin lock”。由于移动端通常 CPU 核数较少(2-8 核),偏向锁的收益不如在服务器端显著——撤销偏向锁需要全局安全点,这在低延迟要求的移动应用中代价较高。

ART 在 Android 8.0(Oreo)前后对锁实现做了重要改进:

  1. 锁膨胀的批量处理MonitorPool 不再为每个对象单独分配 Monitor,而是维护一个全局的 Monitor 池(art/runtime/monitor_pool.cc),通过 CAS 竞争分配。这减少了内存碎片和分配开销。

  2. thin lock 的优化:ART 的 thin lock 支持直接存储 Lock Record 指针而不走 Monitor,避免了 Monitor 对象的堆分配。thin lock to fat lock 的转换仅在调用 wait/notify 或线程竞争超时时发生。

  3. AOT 编译优化:ART 的 optimizing compiler 在 art/compiler/optimizing/ 中对单线程模式下的锁操作做消除优化(lock elision)。如果 AOT 编译器通过逃逸分析确认一个同步对象不会被多个线程访问到,可以直接消除 monitorenter/monitorexit 指令。

七、死锁检测机制

ART 的 Monitor 机制包含基本的死锁检测能力。当线程在 Monitor::MonitorLock 中等待锁时,可以检查锁的持有者链:

Thread A 持有 Monitor1 ,等待 Monitor2
Thread B 持有 Monitor2 ,等待 Monitor1
→ 如果 Thread A 等待 Monitor2 时发现 Monitor2 的 owner 是 Thread B,
而 Thread B 又在等待 Monitor1(Monito1 的 owner 是 Thread A),
则检测到死锁 → 可以通过 SIGQUIT 转储线程堆栈来报告。

这种检测仅在重量级锁层面可行(因为只有重量级锁有 owner_ 字段记录持有者)。轻量级锁的 Lock Record 存储在各自线程的栈上,无法跨线程遍历。因此,死锁检测依赖于锁已膨胀到重量级状态。

八、AOSP 关键源码路径总结

文件 功能
art/runtime/monitor.cc Monitor 核心实现:MonitorEnter/Exit、Wait/Notify/NotifyAll、InflateThinLocked
art/runtime/monitor.h Monitor 类定义:owner_、lock_count_、wait_set_、entry_list_
art/runtime/monitor_pool.cc MonitorPool:全局 Monitor 池,Monitor 的创建和回收
art/runtime/mirror/object.h Object 类定义:klass_、monitor_(LockWord)
art/runtime/mirror/object.cc Object 方法:GetLockWord、SetLockWord、IdentityHashCode 惰性计算
art/runtime/base/mutex.h Mutex 和 ConditionVariable(封装 futex)
art/runtime/interpreter/interpreter_common.cc 解释器处理 OP_MONITOR_ENTER、OP_MONITOR_EXIT
art/runtime/thread_list.cc ThreadList::SuspendAll(安全点/线程挂起机制)
art/compiler/optimizing/ 锁粗化、锁消除等编译优化 pass

九、ART 解释器中的 monitorenter/monitorexit 实现

这部分展示 ART 解释器如何逐条处理 monitorenter 和 monitorexit 指令,从而将字节码语义映射到 Monitor 接口。

9.1 monitorenter 的解释器路径

art/runtime/interpreter/interpreter_common.cc 中,OP_MONITOR_ENTER 的处理逻辑(简化)如下:

OP_MONITOR_ENTER:
// 1. 从操作数栈弹出锁对象引用
Object* obj = shadow_frame.GetVReg(operand);

// 2. 对 null 做特殊处理
if (UNLIKELY(obj == nullptr)) {
// 对 null 执行 monitorenter 会抛出 NullPointerException
ThrowNullPointerException("synchronized object is null");
// 跳转到异常处理
}

// 3. 调用 Monitor::MonitorEnter
Monitor::MonitorEnter(self, obj, /*try_spinning=*/true);

// 4. 继续执行下一条指令

Monitor::MonitorEnter 的内部流程(art/runtime/monitor.cc):

Monitor::MonitorEnter(Thread* self, Object* obj, bool try_spinning):
1. 读取对象头的 LockWord
2. 根据 LockWord 的状态分派:
- State::kUnlocked: CAS 尝试安装 thin lock
- State::kThinLocked:
a. 检查 owner 是否就是 self:如果是 → 重入 → displaced_header = 0 → 成功
b. 否则:自旋尝试 CAS 获取 thin lock
c. 自旋失败 → InflateThinLocked → 升级为 fat lock
- State::kFatLocked:
a. 从 MonitorPool 获取 Monitor 对象
b. 检查 owner == self → lock_count_++ → 成功
c. 否则:Monitor::Lock (自旋 + futex_wait if needed)
3. 返回是否成功获取

9.2 monitorexit 的解释器路径

OP_MONITOR_EXIT:
Object* obj = shadow_frame.GetVReg(operand);

if (UNLIKELY(obj == nullptr)) {
ThrowNullPointerException(...);
}

Monitor::MonitorExit(self, obj);

Monitor::MonitorExit 的内部流程:

Monitor::MonitorExit(Thread* self, Object* obj):
1. 读取 LockWord
2. 如果 LockWord 是 thin:
a. 读取当前栈帧顶部的 Lock Record
b. 如果 displaced_header == 0 → 重入退出 → 弹出 Lock Record → 结束
c. 否则:CAS 将 displaced_header 写回对象的 Mark Word
d. 如果 CAS 成功 → 释放成功
e. 如果 CAS 失败 → 锁已膨胀 → 走 fat lock 释放路径
3. 如果 LockWord 是 fat:
a. Monitor::Unlock → lock_count_--
b. 如果 lock_count_ == 0 → 从 cxq_/entry_list_ 唤醒下一个等待者
4. 如果 LockWord 状态不对(不是 thin 也不是 fat)→ 抛出 IllegalMonitorStateException

9.3 嵌套 monitorenter/monitorexit 的 Lock Record 栈

每个线程的栈帧维护一个隐式的 Lock Record 栈:

Thread's stack frame:
┌──────────────────────┐
│ Lock Record (obj2) │ ← 最内层 synchronized(obj2)
│ displaced_header=0 │ (重入 obj2)
├──────────────────────┤
│ Lock Record (obj2) │ ← synchronized(obj2)
│ displaced_header= │ (首次进入 obj2)
│ original MW │
├──────────────────────┤
│ Lock Record (obj1) │ ← synchronized(obj1)
│ displaced_header= │
│ original MW │
├──────────────────────┤
│ ...其他栈帧... │
└──────────────────────┘

每个 synchronized 块的进入在栈顶压入一个 Lock Record;退出时弹出。如果要退出的 Lock Record 的 displaced_header 不是重入标记(!= 0),则需要与对象头的 Mark Word 做 CAS 交互来真正释放锁。

十、安全点与线程挂起机制

偏向锁撤销、批量重偏向/撤销、GC 根枚举等操作都依赖 ART 的线程挂起机制。核心实现在 art/runtime/thread_list.ccThreadList::SuspendAll 中。

10.1 安全点(Safepoint)的工作原理

安全点不是一条特定的指令,而是一个协作式协议

  1. 请求方(如偏向锁撤销逻辑)调用 ThreadList::SuspendAll(),设置一个全局标志(safepoint flag)。
  2. 每个运行的线程在特定位置(如方法返回、循环回边、JNI 调用边界)检查这个标志。
  3. 当线程发现 safepoint flag 被设置时,它将自己挂起(通过 futex wait 或 pthread_cond_wait),并将控制权交还给请求方。
  4. 请求方等待所有线程都到达安全点后,执行需要全局暂停的操作(如批量重偏向、GC)。
  5. 操作完成后,请求方清除 safepoint flag 并调用 ThreadList::ResumeAll() 唤醒所有线程。

10.2 安全点对 Android 应用的影响

安全点暂停(Stop-The-World, STW)是偏向锁撤销的最大成本:

  • 延迟:一个缓慢到达安全点的线程(如在执行大型 memcpy 或在内核中等待 I/O)会拖慢所有其他线程。
  • 频率:频繁的偏向锁撤销意味着频繁的 STW 暂停,这会直接表现为 UI 卡顿。
  • ART 的策略:正是由于这种 STW 开销,ART 在移动端对偏向锁持谨慎态度——移动应用的帧渲染通常在 16ms 预算内,一次意外的 safepoint 暂停就可能导致掉帧。

十一、LockWord 状态转换实战

以下通过具体的 32 位 hex 值展示 LockWord 在锁升级过程中的变化:

11.1 状态转换序列

初始状态(无锁,刚创建的对象):
Mark Word = 0x0000_0001 (hash_code=0, age=0, lock=01, biased_lock=0)

首次调用 hashCode():
Mark Word = 0x3AF4_18C1 (hash_code=0x3AF418C, age=0, lock=01)
↑ 此时该对象永远不能获得偏向锁(hash 区域已被占用)

==================== 如果没有调用 hashCode() ====================

偏向锁安装(线程 T1 首次获取):
修改前: Mark Word = 0x0000_0005 (age=0, lock=101, biased_lock=1, thread_id=0)
CAS后: Mark Word = 0x04A1_2405 (thread_id=T1, epoch=0, age=0, lock=101)

T1 重入(无需 CAS,直接比对 thread_id):
Mark Word = 0x04A1_2405 (不变!零开销)

T2 尝试获取(撤销偏向 → 升级轻量级锁):
safepoint ...
修改前: Mark Word = 0x04A1_2405 (偏向 T1)
修改后: Mark Word = 0xB4C8_D100 (指向 T1 栈上 Lock Record 的指针, lock=00)

T2 自旋 CAS 失败 → 膨胀为重量级锁:
修改后: Mark Word = 0x8F3A_0012 (指向 Monitor 对象的指针, lock=10)

11.2 实战辨识技巧

在调试 crash 或线程 dump 时,可以通过 Mark Word 的低 3 位快速判断对象当前的锁状态:

Mark Word 低 3 位 锁状态
.... .... .... .001 (biased_lock=0) 无锁
.... .... .... .101 (biased_lock=1) 偏向锁
.... .... .... .000 轻量级锁
.... .... .... .010 重量级锁
.... .... .... .011 GC 转发指针

十二、synchronized vs JUC 锁 性能对比

12.1 不同场景下的性能特征

场景 synchronized ReentrantLock 说明
单线程重复获取 极快(偏向锁消除所有开销) 需要 CAS(约 10-15ns) synchronized 胜出
双线程低竞争 轻量级锁 CAS 自旋,与 ReentrantLock 相当 CAS 获取,成功则无需系统调用 势均力敌
高竞争(>2 线程) 膨胀为重量级锁,futex 阻塞 futex 阻塞,但有公平/非公平模式可选 ReentrantLock 可选公平策略
需要 tryLock / 定时获取 不支持 支持(tryLock, tryLock(time, unit)) ReentrantLock 唯一选择
需要 Condition 多条件等待 不支持(只有单条件 wait/notify) 支持(多个 Condition 实例) ReentrantLock 唯一选择
需要中断等待 不支持(不可中断) 支持(lockInterruptibly) ReentrantLock 唯一选择
锁状态监控 有限(通过线程 dump) 丰富(getQueueLength, hasQueuedThreads, isLocked) ReentrantLock 更灵活

12.2 选择建议

优先使用 synchronized,除非:

  • 需要 tryLock(尝试获取,不阻塞)、定时获取或可中断的锁获取。
  • 需要多个等待条件(如生产者-消费者中区分”非空”和”非满”两个条件)。
  • 需要公平锁(FIFO 保证,避免线程饥饿)。
  • 需要运行时监控锁的队列长度和等待状态。

选择 synchronized 的理由:

  • 语法简洁,异常自动释放(不需要 finally unlock)。
  • JVM 持续优化(锁粗化、锁消除、自适应自旋),这些优化对 synchronized 更成熟。
  • ART 的 thin lock 快速路径在低竞争移动场景中通常优于 ReentrantLock 的 CAS 路径。

十三、常见锁问题的调试方法

13.1 死锁检测:通过线程 Dump

死锁的典型 ANR 堆栈特征:

"Thread-2" prio=5 tid=12 Blocked
waiting for monitor on Object@a6f3b10 held by Thread-1
at com.example.Foo.bar(Foo.java:42)

"Thread-1" prio=5 tid=11 Blocked
waiting for monitor on Object@b8e4c21 held by Thread-2
at com.example.Foo.baz(Foo.java:58)

如果 Thread-1 等待锁 A(被 Thread-2 持有),同时 Thread-2 等待锁 B(被 Thread-1 持有),则形成经典死锁环。

13.2 通过 ANR trace 分析锁争用

Android ANR trace 文件(/data/anr/traces.txt)中的关键信息:

"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 obj=0x74a0b0f8 self=0x7f8a3c5000
| sysTid=12345 nice=-10 cgrp=default sched=0/0 handle=0x7f8c0fe000
| state=S schedstat=( ... ) utm=320 stm=48 core=0 HZ=100
| stack=0x7fda708000-0x7fda70a000 stackSize=8MB
| held mutexes=
at com.example.HeavyWork.compute(HeavyWork.java:23)
- waiting to lock <0x0a6f3b10> (a com.example.DataCache) held by thread 12

"Thread-12" prio=5 tid=12 Runnable
| held mutexes= "mutex lock for <0x0a6f3b10>"
at com.example.DataCache.update(DataCache.java:78)
...

关键信息提取:

  • Blocked 状态表示线程正在等待获取 monitor(synchronized)。
  • waiting to lock <address> 指明了等待的具体锁对象和地址。
  • held by thread N 指明了当前持有者。
  • held mutexes= 列出了该线程当前持有的所有锁。

13.3 Android Studio Profiler 的锁监控

Android Studio 的 CPU Profiler 支持记录和可视化锁争用:

  • 使用 Debug.startMethodTracingSampling() 或 CPU Profiler 中的 “Java/Kotlin Method Trace” 模式。
  • 锁争用事件显示为 “Monitor Contention”。
  • 可以查看每个线程在每个锁上的等待时间,快速识别热点锁。

十四、AOSP 关键源码路径总结(续)

文件 功能
art/runtime/thread.cc Thread 类:safepoint 检查、线程状态转换
art/runtime/thread_list.cc ThreadList::SuspendAll/ResumeAll(安全点/线程挂起)
art/runtime/runtime.cc Runtime 初始化:MonitorPool 和锁系统的启动
art/runtime/native/java_lang_Object.cc Object.wait/notify/notifyAll 的 JNI 注册
art/runtime/entrypoints/quick/quick_trampoline_entrypoints.cc JIT 编译代码中的锁 entrypoint
libcore/ojluni/src/main/java/java/lang/Object.java Java 层 Object.wait/notify/notifyAll 声明

十五、深入 futex:从 Java 到 Linux 内核的完整路径

为了彻底理解 synchronized 的性能特征,以下追踪一个 futex 等待/唤醒的完整系统调用链:

15.1 Wait 路径(Thread.wait → FUTEX_WAIT)

Java:     obj.wait()

JNI: java_lang_Object_wait (art/runtime/native/java_lang_Object.cc)

ART: Monitor::Wait (art/runtime/monitor.cc)

ART: ConditionVariable::Wait (art/runtime/base/mutex.h)

Bionic: __futex_wait (bionic/libc/bionic/futex.cpp)

Kernel: syscall(SYS_futex, uaddr, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, val, timeout)

┌─────────────────────────────────────────────────┐
│ Kernel: 将当前线程从 runqueue 中移除 │
│ 将其挂接到 futex wait queue │
│ 调度器切换到其他可运行线程 │
└─────────────────────────────────────────────────┘

15.2 Wake 路径(Thread.notify → FUTEX_WAKE)

Java:     obj.notify()

JNI: java_lang_Object_notify

ART: Monitor::Notify → ConditionVariable::Signal

Bionic: __futex_wake

Kernel: syscall(SYS_futex, uaddr, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1)

┌─────────────────────────────────────────────────┐
│ Kernel: 从 futex wait queue 取出 1 个线程 │
│ 将其加入 runqueue │
│ 调度器在合适时机将其投入运行 │
└─────────────────────────────────────────────────┘

15.3 性能数据(近似值,x86-64 Linux)

操作 耗时 说明
单体 CAS(无竞争) ~10-15 ns 纯用户态,单条 CPU 指令
futex WAIT(无阻塞) ~100-200 ns 需要系统调用但立即返回
futex WAIT(阻塞) ~1-5 us 包括上下文切换
futex WAKE ~500-1000 ns 系统调用 + 被唤醒线程需竞争调度
偏向锁获取 ~1-2 ns 仅一条内存读取(比对 thread_id)
轻量级锁获取(CAS 成功) ~10-20 ns 一次 CAS 操作
轻量级锁获取(CAS 失败,需自旋) ~50-500 ns 取决于自旋次数
重量级锁获取(无竞争) ~20-50 ns 一次 CAS 将对象头指向 Monitor

这些数据揭示了为什么偏向锁在单线程场景下有压倒性优势(1-2ns vs 10-20ns),以及为什么锁升级是一条不归路——一旦进入重量级锁,每次获取都需要 futex 系统调用。


面试问答

Q1:synchronized 在字节码层面有哪两种表示方式?JVM 如何保证异常情况下锁一定被释放?

A:同步代码块使用 monitorenter / monitorexit 指令对;同步方法在 access_flags 中设置 ACC_SYNCHRONIZED 标志(0x0020),JVM 在方法入口隐式获取锁、方法出口隐式释放锁。对于代码块方式,编译器生成异常表,包含覆盖同步块全部范围的 catch-all 处理器(from...to...target type any)。当同步块内抛出异常时,执行流转到异常处理器,先执行 monitorexit 释放锁,再通过 athrow 重新抛出。异常处理器自身也有嵌套异常保护(第二个异常表条目),确保即使 monitorexit 出现异常也能保证正确的异常传播。对于同步方法,释放锁由 JVM 在方法退出时自动保证,包括异常退出。

Q2:简述 synchronized 锁升级的完整路径,每级锁的标志位是什么?

A:锁升级路径为:无锁(001)→ 偏向锁(101)→ 轻量级锁(000)→ 重量级锁(010)。无锁状态下,Mark Word 存储 hashCode 和 GC 分代年龄。偏向锁状态下,Mark Word 存储持有该锁偏向的线程 ID,同一线程再次进入时无需任何同步操作——仅读一次 Mark Word 比对 thread ID 即可。当另一线程尝试获取已偏向的锁时,在安全点撤销偏向,升级为轻量级锁。轻量级锁通过线程栈上的 Lock Record 和 CAS 自旋争夺,Mark Word 存储 Lock Record 指针。当自旋超时、等待线程数超过阈值或调用 wait() 时,锁膨胀为重量级锁,Mark Word 指向 ObjectMonitor 对象,底层通过 futex 实现阻塞和唤醒。锁不会自动降级(HotSpot 中在 safepoint 可能有 deflation,ART 中 MonitorPool 空间紧张时会回收闲置 Monitor,但这属于资源回收而非语义降级)。

Q3:Object.wait() / notify() 在 ART 中是如何实现的?跟 futex 的关系是什么?

A:Object.wait() 的核心逻辑在 art/runtime/monitor.ccMonitor::Wait 方法中。第一步验证当前线程持有 Monitor(owner_ == self),否则抛出 IllegalMonitorStateException。第二步释放 Monitor,将 lock_count_ 清零,唤醒 entry_list 中的下一个等待者。第三步将当前线程加入 Monitor 的 wait_set_ 链表。第四步在条件变量 monitor_cont_ 上调用 Wait 阻塞。notify / notifyAllwait_set_ 中取出一个或全部线程,通过 monitor_cont_.Signal / Broadcast 唤醒。monitor_cont_ 的类型是 ConditionVariableart/runtime/base/mutex.h),其内部使用 futex(FUTEX_WAIT)futex(FUTEX_WAKE) 系统调用。futex 的优势在于无竞争时完全在用户态操作一个 32 位锁变量,不需要进入内核;只有需要阻塞等待或唤醒时才通过系统调用进入内核。被 notify 唤醒的线程不会立即执行,而是先进入 entry_list 竞争重新获取 Monitor——这意味着 notify 和实际执行之间可能存在时间窗口,其他线程在此期间可能先获取锁。

Q4:为什么偏向锁在 Android 上可能被禁用?

A:偏向锁通过减少无竞争场景下的同步开销来提高性能,但它的撤销操作需要全局安全点(GC safepoint),即暂停所有线程。在服务器端大量单线程使用锁组件(如线程池、集合类)的场景下,偏向锁收益明显。但在 Android 移动端,CPU 核数少(2-8 核),safepoint 延迟对 UI 帧率和响应时间的影响更显著,且移动应用的线程模型相对简单,偏向锁的收益不如 HotSpot 服务端。因此 ART 在早期版本(Android O 之前)默认关闭偏向锁,后期版本虽然逐步引入,但其实现比 HotSpot 更为保守。开发者不需要关心这一细节——synchronized 语义由 JVM 保证,偏向锁只是运行时性能优化策略,不影响正确性。

Q5:锁粗化和锁消除的工作原理分别是什么?什么条件下会触发?

A:锁粗化由 JIT 编译器执行。当编译器检测到连续的、操作同一锁对象的 synchronized 块时,将这些小块合并为一个大的同步块,减少了 monitorenter/monitorexit 的频次。触发条件是在某个方法内短时间间隔内对同一对象有多次 synchronized 访问——编译器基于控制流分析判断这些块之间没有可能跳转到其他持有同一锁的代码。锁消除也由 JIT 编译器执行,前提是逃逸分析证明同步对象不会被其他线程访问(对象不逃逸当前栈帧)。典型场景是 StringBuffer 在方法局部变量中的使用:StringBuffer 的每个方法都是 synchronized,但 JIT 分析发现 sb 对象不逃逸→消除所有 sb 方法中的锁操作。两者在 ART 的 optimizing compiler 中都有对应实现(art/compiler/optimizing/)。

Q6:synchronized 支持可重入是如何实现的?轻量级锁和重量级锁在重入时的处理有何不同?

A:synchronized 的可重入性保证了同一线程可以多次进入同一锁保护的区域而不会死锁。在重量级锁(Monitor)层面,通过 lock_count_ 字段计数:首次获取时 lock_count_ = 1, owner_ = self;重入时检查 owner_ == self 后仅递增 lock_count_,不需要任何 CAS 或 futex 操作;退出时递减 lock_count_,仅在 lock_count_ == 0 时才释放锁。在轻量级锁层面,通过 Lock Record 的 displaced_header 字段判别:首次获取时 displaced_header 存储原始 Mark Word;重入时新的 Lock Record 的 displaced_header = 0 作为重入标记;退出时若 displaced_header != 0 则 CAS 写回(释放锁),若为 0 则仅弹出 Lock Record(退出一个嵌套层)。两种实现都确保了重入的 O(1) 开销。

打赏
  • 微信
  • 支付宝

评论