Java并发-synchronized实现原理
Java并发-synchronized实现原理
xukunSynchronized的实现原理
synchronized 是 Java 并发编程的基石,理解其底层原理对于编写高性能并发程序至关重要。
一、核心概念
synchronized 实现原理依赖于 JVM 的 Monitor(监视器锁)和 对象头(Object Header)。当 synchronized 修饰在方法或代码块上时,会对特定的对象或类加锁,从而确保同一时刻只有一个线程能执行加锁的代码块。
1.1 synchronized 的三种使用方式
| 使用方式 | 锁对象 | 示例 |
|---|---|---|
| 修饰实例方法 | 当前实例对象 this | public synchronized void method() |
| 修饰静态方法 | 当前类的 Class 对象 | public static synchronized void method() |
| 修饰代码块 | 指定的锁对象 | synchronized(lock) { } |
底层字节码实现差异:
1 | // 1. synchronized 修饰方法 |
通过 javap -v 反编译可以看到:
1 | // 代码块方式的字节码 |
注意:编译器会自动生成两个
monitorexit指令,一个用于正常退出,一个用于异常退出,确保锁一定会被释放,避免死锁。
二、对象头(Object Header)深度解析
在 HotSpot JVM 中,每个对象的内存布局由三部分组成:
1 | |----------------------------------------------| |
2.1 Mark Word 详解
Mark Word 是实现 synchronized 的关键,它会根据锁的状态动态存储不同的信息。以 64 位 JVM 为例:
| 锁状态 | 存储内容 | 标志位 |
|---|---|---|
| 无锁 | 对象 HashCode、GC 分代年龄 | 01 |
| 偏向锁 | 偏向线程 ID、偏向时间戳、GC 分代年龄 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向 Monitor 对象的指针 | 10 |
| GC 标记 | 空 | 11 |
1 | |-------------------------------------------------------|--------------------| |
三、Monitor(监视器锁)机制
3.1 Monitor 结构
每个 Java 对象都可以关联一个 Monitor 对象。在 HotSpot 虚拟机中,Monitor 是由 ObjectMonitor 实现的(C++ 代码):
1 | // hotspot/src/share/vm/runtime/objectMonitor.hpp |
3.2 Monitor 工作流程
1 | +------------------+ |
核心流程:
- 线程尝试获取锁时,先通过 CAS 设置
_owner为当前线程 - 若 CAS 失败,进入
_EntryList阻塞队列等待 - 持有锁的线程调用
wait()后释放锁,进入_WaitSet等待被唤醒 - 被
notify()/notifyAll()唤醒后,重新进入_EntryList竞争锁
四、锁升级机制详解
JDK 1.6 开始,为了减少获取锁和释放锁带来的性能消耗,引入了 偏向锁 和 轻量级锁,锁共有四种状态:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
重要:锁只能单向升级,不能降级(GC 时除外)。这是为了避免频繁升降级带来的性能开销。
4.1 偏向锁(Biased Locking)
设计思想:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁就是为了让线程在无竞争场景下,消除同步原语,进一步提高性能。
加锁过程:
- 当线程第一次访问同步块时,检查 Mark Word 中是否存储了当前线程 ID
- 如果没有,通过 CAS 将当前线程 ID 写入 Mark Word
- 成功后,后续该线程进入同步块只需检查线程 ID 是否匹配,无需 CAS
偏向锁撤销:
- 当有另一个线程尝试竞争偏向锁时,持有偏向锁的线程会被挂起
- JVM 在安全点(Safe Point)暂停该线程,检查其是否仍处于同步块中
- 若已退出同步块,则撤销偏向锁,恢复到无锁状态
- 若仍在同步块中,则升级为轻量级锁
1 | // 可通过 JVM 参数控制偏向锁 |
注意:JDK 15 开始,偏向锁默认禁用,JDK 18 彻底移除。因为现代 JVM 的 CAS 操作已经足够高效。
4.2 轻量级锁(Lightweight Locking)
设计思想:适用于多个线程交替执行同步块,但不存在竞争的场景。通过 CAS 和自旋来避免线程阻塞。
加锁过程:
- JVM 在当前线程的栈帧中创建
Lock Record(锁记录) - 将对象头的 Mark Word 复制到 Lock Record 中(称为 Displaced Mark Word)
- 使用 CAS 将对象头的 Mark Word 替换为指向 Lock Record 的指针
- CAS 成功,获得轻量级锁;CAS 失败,进入自旋或膨胀为重量级锁
1 | 栈帧 对象头 |
自旋优化:
1 | // 自适应自旋:根据历史成功率动态调整自旋次数 |
4.3 重量级锁(Heavyweight Locking)
当自旋超过阈值或竞争激烈时,轻量级锁膨胀为重量级锁。此时会使用操作系统的 互斥量(Mutex) 实现线程阻塞和唤醒。
性能开销:涉及用户态到内核态的切换,开销较大(约 1-10 微秒)。
4.4 锁升级流程图
1 | 首次访问同步块 |
五、Synchronized 的可重入性
synchronized 是可重入锁,同一线程可以多次获取同一把锁而不会死锁。
实现原理:Monitor 中的 _recursions 计数器记录重入次数:
- 每次获取锁:计数器 +1
- 每次释放锁:计数器 -1
- 计数器归零时:真正释放锁
1 | public class ReentrantDemo { |
六、synchronized 与 ReentrantLock 对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置(字节码指令) | JDK 实现(AQS) |
| 锁获取方式 | 隐式获取释放 | 显式 lock()/unlock() |
| 可中断 | 不支持 | 支持 lockInterruptibly() |
| 超时获取 | 不支持 | 支持 tryLock(timeout) |
| 公平锁 | 非公平 | 支持公平/非公平 |
| 条件变量 | 单一(wait/notify) | 多个 Condition |
| 锁绑定对象 | 锁对象 | Lock 实例 |
七、实战建议
- 优先使用 synchronized:代码简洁,JVM 会自动优化,不易出错
- 需要高级特性时用 ReentrantLock:如可中断、超时、公平锁、多条件变量
- 减小锁粒度:只对必要的代码块加锁,避免持锁时间过长
- 避免嵌套锁:容易产生死锁,如必须嵌套则保持获取锁的顺序一致
1 | // 推荐:缩小锁范围 |











