synchronized 实现原理
1. 对象头与 Mark Word
每个 Java 对象在内存中分为三部分:对象头、实例数据 和 对齐填充。
对象头 是核心部分,包含以下信息:
- Mark Word(标记字段):存储对象的哈希码、分代年龄、锁状态等。
- Klass Pointer(类型指针):指向类的元数据(方法区中的 Class 对象)。
Mark Word 的结构(以 64 位 JVM 为例):
锁状态 | 存储内容(64位) |
---|---|
无锁 | 哈希码(25位) + 分代年龄(4位) + 偏向模式(1位,0) + 锁标志位(2位,01) |
偏向锁 | 线程ID(54位) + 时间戳(2位) + 分代年龄(4位) + 偏向模式(1位,1) + 锁标志位(01) |
轻量级锁 | 指向栈中锁记录的指针(62位) + 锁标志位(00) |
重量级锁 | 指向 Monitor 的指针(62位) + 锁标志位(10) |
GC 标记 | 空(无意义) + 锁标志位(11) |
锁标志位(2位) 决定了当前锁的状态:01
(无锁/偏向锁)、00
(轻量级锁)、10
(重量级锁)、11
(GC标记)。
2. Monitor 机制
每个对象关联一个 Monitor(监视器锁),其本质是 JVM 对操作系统 Mutex Lock 的封装,用于管理线程的竞争与等待。
Monitor 的结构如下:
- Owner:持有锁的线程。
- EntryList:等待锁的线程队列(未获取到锁的线程在此阻塞)。
- WaitSet:调用
wait()
后进入等待状态的线程队列。
线程获取锁的流程:
- 线程尝试通过 CAS 操作 将 Mark Word 中的锁标志位设置为轻量级锁或偏向锁。
- 若竞争失败,线程进入 EntryList 等待,并升级为重量级锁(依赖操作系统 Mutex Lock)。
- 持有锁的线程释放锁后,唤醒 EntryList 中的线程重新竞争。
3. 锁升级过程
为提高性能,JVM 在 JDK 1.6 后引入 锁升级 机制,根据竞争情况动态调整锁状态:
-
无锁
- 初始状态,未发生线程竞争。
-
偏向锁
- 适用场景:单线程重复访问同一同步代码。
- 实现:将线程ID写入 Mark Word,后续访问无需 CAS 操作。
- 触发升级:当其他线程尝试获取锁时,撤销偏向锁,升级为轻量级锁。
-
轻量级锁
- 适用场景:低并发、线程交替执行同步代码。
- 实现:通过 CAS 自旋 尝试获取锁,避免线程阻塞。
- 触发升级:自旋超过阈值(默认 10 次)或竞争加剧时,升级为重量级锁。
-
重量级锁
- 适用场景:高并发、竞争激烈。
- 实现:依赖操作系统 Mutex Lock,线程阻塞并进入 EntryList 等待唤醒。
- 开销:涉及用户态到内核态的切换,性能较低。
4. 字节码层面的实现
synchronized
在字节码中通过两条指令实现同步:
- 同步代码块:
编译后插入monitorenter
和monitorexit
指令,分别对应锁的获取和释放。public void method() {synchronized (this) { // monitorenterSystem.out.println("同步代码块");} // monitorexit }
- 同步方法:
方法标记ACC_SYNCHRONIZED
标志,JVM 在方法调用时隐式获取锁。public synchronized void increase() { count++; }
5. 其他核心特性
-
可重入性
同一线程可多次获取同一把锁(通过计数器实现,避免死锁)。 -
排他性
同一时刻仅一个线程能持有锁,其他线程必须等待。 -
锁释放
线程执行完同步代码块或抛出异常时,JVM 自动释放锁。
总结
synchronized
的底层实现结合了 对象头标记、Monitor 竞争管理 和 锁升级优化,在保证线程安全的同时兼顾性能。其核心优势在于:
- JVM 自动管理锁:无需手动释放,避免死锁。
- 锁升级机制:根据竞争动态调整,减少性能开销。
- 可重入性:支持同一线程重复获取锁。
适用场景:单例模式、简单资源同步等低竞争或需快速实现同步的场景。对于高并发复杂场景,可结合 Lock
的高级功能(如超时、公平锁)优化性能。