目录
  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 Mark Word 的位布局
  3. 3. 三、锁升级(Lock Escalation)的完整路径
    1. 3.1. 3.1 偏向锁(Biased Lock)
    2. 3.2. 3.2 轻量级锁(Thin Lock / Lightweight Lock)
    3. 3.3. 3.3 重量级锁(Heavyweight Lock / Fat Lock)
    4. 3.4. 3.4 关于锁降级
  4. 4. 四、Object.wait / notify 与 futex
    1. 4.1. 4.1 wait/notify 的 Monitor 实现
    2. 4.2. 4.2 futex 原理
  5. 5. 五、重入性(Reentrancy)与锁计数
  6. 6. 六、偏向锁在 Android ART 中的特殊处理
  7. 7. 面试问答
【深入理解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 需要再从局部变量加载。

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

第三,异常处理路径中还有一个嵌套的异常处理器(from 13 to 16 target 13 type any),用于处理 monitorexit 自身可能抛出的异常(虽然理论上不会发生,但 JVM 规范要求此处理器存在)。

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)

二、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_; // 锁字 / Mark 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 指针)。

2.2 Mark Word 的位布局

monitor_ 字段(也称为 Lock Word 或 Mark Word)是一个多用途字段,根据低位的锁标志(lock state bits)解释为不同含义。以 32 位 JVM 为例:

锁状态 位[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 期间的转发指针

在 64 位 ART 中,Mark Word 有 32 位或 64 位两种形式,遵循类似的位布局逻辑但更复杂。这一状态机决定了锁升级的路径。

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

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

3.1 偏向锁(Biased Lock)

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

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

获取过程

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

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

Android ART 中偏向锁的实现值得注意。由于移动设备通常 CPU 核数较少且线程模型较简单,部分 Android 版本默认关闭了偏向锁。在 ART 中,偏向锁相关逻辑位于 art/runtime/monitor.ccMonitor::InflateThinLocked 函数处理从 thin lock 到 fat lock 的升级(ART 将偏向锁称为 biased locking,轻量级锁称为 thin lock)。

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

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

获取过程

  1. 在线程栈上分配一个 Lock Record 空间。
  2. 将对象的 Mark Word 拷贝到 Lock Record 中(保留原始的 hash code 等信息)。
  3. 通过 CAS 操作将对象的 Mark Word 更新为指向该 Lock Record 的指针。
  4. 如果 CAS 成功,表示获取轻量级锁成功。如果 CAS 失败,进入自旋等待或锁膨胀。

自旋(Spinning):CAS 失败的线程不会立即阻塞,而是执行一定次数的空循环(busy loop),期望持有锁的线程在自旋期间释放锁。自旋次数通常根据前一次在同一个锁上的自旋成功率和当前 CPU 负载动态调整(自适应自旋)。JVM 参数 -XX:PreBlockSpin 控制初始自旋次数(默认 10),-XX:+UseSpinning 启用自旋(Java 7 之后默认开启)。

在 ART 的源码 art/runtime/monitor.cc 中,Monitor::MonitorLock 函数在获取重量级锁时也包含了自旋逻辑的尝试(先尝试 fast path,失败后再走 futex 阻塞)。

3.3 重量级锁(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 替换...
}

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() 的线程)
};

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

3.4 关于锁降级

重要事实:synchronized 锁不会降级。一旦膨胀为重量级锁,即使竞争消失,锁也不会回退为轻量级锁或偏向锁。只有在 GC 的安全点,JVM 才能安全地检查锁状态并执行降级(HotSpot 在某些 GC 周期中实现了一种有限的批量降级,被称为「bulk revoke bias」和「deflation」)。ART 中的 Monitor 对象有一个 Deflate 机制,在 MonitorPool 空间紧张时回收不再被使用的 Monitor 对象,但这不等同于运行时锁降级。

四、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。
    2. 释放 Monitor(通过 Monitor::Unlock),将 lock_count_ 保存后清零。
    3. 将当前线程加入 Monitor 的 wait_set_ 链表。
    4. 在条件变量 monitor_cont_ 上等待(Waitfutex(FUTEX_WAIT))。
    5. 被唤醒后,重新获取 Monitor(Monitor::Lock),恢复 lock_count_
  • **Monitor::Notify**:

    1. wait_set_ 中取出一个线程(通常是从头部)。
    2. 通过 monitor_cont_.Signal 唤醒该线程(底层 futex(FUTEX_WAKE) 唤醒一个线程)。
  • **Monitor::NotifyAll**:

    1. 遍历整个 wait_set_,通过 monitor_cont_.Broadcast 唤醒所有等待线程(futex(FUTEX_WAKE, INT_MAX))。

4.2 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)

五、重入性(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 并真正释放锁。

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

synchronized (obj) {
synchronized (obj) {
// 这里 lock_count_ = 2
}
// 这里 lock_count_ = 1
}

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

六、偏向锁在 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 指令。


面试问答

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

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

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

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

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

A:Object.wait() 的核心逻辑在 art/runtime/monitor.ccMonitor::Wait 方法中。首先释放 Monitor,清零 lock_count_;然后将当前线程加入 Monitor 的 wait_set_ 链表;最后在条件变量 monitor_cont_ 上调用 Waitnotify / notifyAllwait_set_ 中取出一个或全部线程,通过 monitor_cont_.Signal / Broadcast 唤醒。monitor_cont_ 的类型是 ConditionVariableart/runtime/base/mutex.h),其内部使用 futex(FUTEX_WAIT)futex(FUTEX_WAKE) 系统调用。futex 的优势在于无竞争时完全在用户态操作一个 32 位锁变量,不需要进入内核;只有需要阻塞等待或唤醒时才通过系统调用进入内核。wait/notify/notifyAll 都必须在持有对象锁的同步块中调用,如果未持有则抛出 IllegalMonitorStateException——这个检查在 Monitor 方法入口处通过 owner_ != self 判定实现。

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

A:偏向锁通过减少无竞争场景下的同步开销来提高性能,但它的撤销操作需要全局安全点(GC safepoint),即暂停所有线程。在服务器端大量单线程使用锁组件(如线程池、集合类)的场景下,偏向锁收益明显。但在 Android 移动端,CPU 核数少(2-8 核),safepoint 延迟对帧率和响应时间的影响更显著,且移动应用的线程模型相对简单,偏向锁的收益不如 HotSpot 服务端。因此 ART 在某些版本中默认关闭偏向锁或减薄偏向锁的逻辑。开发者可以通过 boot.oat 的编译选项评估偏向锁的开关。这对代码无影响——synchronized 语义由 JVM 保证,偏向锁只是运行时优化策略,不影响正确性。

打赏
  • 微信
  • 支付宝

评论