Java 中 synchronized
的实现依托 JVM 的监视器锁(Monitor)机制 与 对象头(Object Header)结构,并随 JDK 版本演进优化,引入锁升级策略(偏向锁、轻量级锁、重量级锁 )。以下从字节码基础、对象头、锁升级、Monitor 原理等维度详细拆解:
一、字节码层面的基础实现
synchronized
修饰 代码块 和 方法 时,字节码实现有差异,但核心依赖 monitorenter
/monitorexit
指令或隐式标记:
1. 同步代码块(显式指定锁对象)
编译后,代码块前后会插入 monitorenter
(加锁)和 monitorexit
(解锁)字节码指令:
monitorenter
:线程执行此指令时,尝试获取锁对象的 Monitor 所有权。若成功,Monitor 计数器 +1;若失败,线程阻塞等待。monitorexit
:执行此指令时,Monitor 计数器 -1。当计数器归 0,锁被释放,唤醒等待线程。
示例(伪字节码):
synchronized (obj) {
// 业务逻辑
}
// 编译后字节码逻辑:
monitorenter // 尝试获取 obj 的锁
// 业务逻辑字节码
monitorexit // 释放 obj 的锁
2. 同步方法(隐式锁)
- 实例方法:编译后方法的访问标志会增加
ACC_SYNCHRONIZED
标记,无需显式monitorenter
/monitorexit
。JVM 调用方法时,自动检查该标记,尝试获取对象实例的 Monitor。 - 静态方法:同样用
ACC_SYNCHRONIZED
标记,但锁对象是类的 Class 对象(如Xxx.class
),保证静态方法的线程安全。
二、对象头与 Mark Word 的关键作用
Java 对象在内存中布局分 对象头、实例数据、对齐填充,其中 对象头的 Mark Word 是 synchronized
实现锁的核心载体:
1. Mark Word 存储结构
Mark Word 存储对象的哈希码、GC 分代年龄、锁状态标志、持有锁的线程 ID 等信息。不同锁状态(无锁、偏向锁、轻量级锁、重量级锁 )下,Mark Word 内容动态变化:
锁状态 | Mark Word 存储内容(简化) | 锁标志位 |
---|---|---|
无锁 | 对象哈希码、分代年龄等 | 01 |
偏向锁 | 偏向线程 ID、分代年龄、偏向标志 | 01(偏向) |
轻量级锁 | 指向线程栈中锁记录的指针 | 00 |
重量级锁 | 指向 Monitor 对象的指针 | 10 |
2. 为什么任意对象可作为锁?
因每个对象都有 Mark Word,可存储锁状态、持有锁的线程 ID 等信息,天然支持作为锁的载体。这也是 synchronized (任意对象)
语法的底层依据。
三、锁升级策略(JDK 1.6+ 优化)
为解决早期 synchronized
直接使用“重量级锁”导致的性能问题,JDK 1.6 引入 偏向锁 → 轻量级锁 → 重量级锁 的升级流程,依据竞争程度动态调整:
1. 偏向锁(无竞争时优化)
- 适用场景:单线程重复获取同一锁(无竞争)。
- 加锁逻辑:
- 线程第一次访问同步块时,JVM 标记对象头的 偏向锁标志(设为 1 ),并记录当前线程 ID 到 Mark Word。
- 后续该线程再次进入同步块,直接对比 Mark Word 中的线程 ID:若匹配,无需 CAS(比较并交换),直接进入代码块(类似“指纹识别” )。
- 优点:消除无竞争场景下的 CAS 开销,提升单线程重复加锁效率。
- 升级触发:若其他线程尝试竞争锁(检测到 Mark Word 中线程 ID 与当前线程不匹配 ),则撤销偏向锁,升级为轻量级锁。
2. 轻量级锁(轻度竞争时优化)
- 适用场景:多线程交替竞争锁(竞争不激烈,无线程阻塞 )。
- 加锁逻辑:
- 线程在栈帧中创建 锁记录(Lock Record),存储当前对象头的 Mark Word 副本。
- 通过 CAS 操作,尝试将对象头的 Mark Word 替换为“指向锁记录的指针”:
- 成功:获取轻量级锁,进入代码块。
- 失败:说明有竞争,JVM 自旋(循环重试 CAS )尝试获取锁;若自旋超过阈值(或竞争加剧 ),升级为重量级锁。
- 优点:用 CAS 替代操作系统级别的线程阻塞,减少上下文切换开销。
- 解锁逻辑:通过 CAS 将对象头的 Mark Word 恢复为原始值(锁记录中的副本 ),若失败说明有竞争,需唤醒等待线程。
3. 重量级锁(激烈竞争时兜底)
- 适用场景:多线程高并发竞争,存在线程阻塞等待。
- 加锁逻辑:
- JVM 为对象分配 Monitor 对象(操作系统级别的同步机制,非 Java 对象 ),并将对象头的 Mark Word 指向 Monitor 地址。
- 线程竞争锁时,若 Monitor 的
Owner
不为空(锁被占用 ),则进入 Entry List 阻塞等待。
- Monitor 结构:
Monitor { Owner: 持有锁的线程(如线程 A 获得锁,Owner = A ) Recursion Count: 重入次数(同一线程重入锁时,计数器 +1 ) Entry List: 等待获取锁的线程队列(竞争失败的线程进入此处阻塞 ) Wait Set: 调用 wait() 后等待唤醒的线程队列(与锁释放无关,是条件等待 ) }
- 解锁逻辑:线程释放锁时,
Recursion Count
减 1;若归 0,Owner
置空,唤醒Entry List
中等待的线程竞争锁。 - 缺点:依赖操作系统内核线程调度,线程阻塞/唤醒涉及 上下文切换,性能开销大。
四、可重入性的实现
synchronized
是可重入锁,同一线程可多次获取同一对象的锁,依赖 Monitor 的 Recursion Count
(重入计数器 ):
- 线程第一次获取锁时,
Recursion Count = 1
,Owner
标记为当前线程。 - 线程再次进入同步块(重入 ),
Recursion Count += 1
,无需阻塞。 - 线程退出同步块时,
Recursion Count -= 1
;当Recursion Count = 0
时,真正释放锁(Owner
置空 )。
作用:避免嵌套同步场景下的死锁(如递归方法加锁、多层方法调用加锁 )。
五、与内存可见性的关联(Happens-Before 规则)
synchronized
保证 “释放锁”的操作 Happens-Before 于“获取同一锁”的操作:
- 线程 A 释放锁前的所有修改,会强制刷入主内存(内存屏障效果 )。
- 线程 B 获取同一锁后,可读取到线程 A 释放锁前的最新值,保证可见性。
这是 synchronized
实现线程安全的基础(原子性靠锁互斥保证,可见性靠内存屏障保证 )。
六、总结:synchronized
实现逻辑全景
- 基础载体:依托对象头的 Mark Word 存储锁状态,Monitor 实现重量级锁的线程调度。
- 锁升级:根据竞争程度,从偏向锁(无竞争)→ 轻量级锁(轻度竞争)→ 重量级锁(激烈竞争)动态升级,平衡性能与并发。
- 核心能力:通过 Monitor 保证原子性(互斥),通过内存屏障保证可见性,通过重入计数器实现可重入。
简单说,synchronized
是 JVM 深度优化的同步机制:无竞争时用偏向锁加速,轻度竞争时用轻量级锁避免阻塞,激烈竞争时用重量级锁兜底,同时通过对象头、Monitor、锁升级策略,实现高效、安全的线程同步。