在Java中,synchronized是用于实现线程同步的关键字,其底层实现原理涉及对象头、监视器锁(Monitor)以及锁升级机制。以下是详细解析:
1. 对象头与Mark Word
每个Java对象在内存中由三部分组成:
- 对象头(Header) :存储对象的元数据,包括锁状态、GC分代年龄等。
- 实例数据(Instance Data) :对象的成员变量。
- 对齐填充(Padding) :确保对象内存对齐。
Mark Word是对象头的一部分,记录了对象的锁状态、哈希码、分代年龄等信息。在32位JVM中,Mark Word占4字节;64位JVM中占8字节。不同锁状态下,Mark Word的内容会动态变化:
| 锁状态 | 存储内容 |
|---|---|
| 无锁 | 哈希码、分代年龄、是否偏向锁(0) |
| 偏向锁 | 线程ID、Epoch、分代年龄、偏向标记(1) |
| 轻量级锁 | 指向栈中锁记录的指针(Lock Record) |
| 重量级锁 | 指向Monitor的指针 |
2. 监视器锁(Monitor)
Monitor是JVM实现的线程同步机制,每个对象关联一个Monitor。当线程执行synchronized代码块时:
- 尝试获取锁:通过CAS操作修改Mark Word,指向当前线程的Monitor。
- 获取成功:线程持有锁,执行同步代码。
- 获取失败:线程进入阻塞队列,等待锁释放后被唤醒。
3. 锁升级机制
为了提高性能,JVM在JDK 6后引入锁升级机制,按竞争激烈程度逐步升级:
-
偏向锁(Biased Lock)
- 适用场景:单线程重复访问同步代码。
- 实现:Mark Word记录线程ID,后续无需CAS操作。
- 升级条件:检测到其他线程尝试获取锁(撤销偏向锁)。
-
轻量级锁(Lightweight Lock)
- 适用场景:多线程交替执行,无实际竞争。
- 实现:通过CAS将Mark Word替换为指向线程栈中锁记录的指针。
- 升级条件:CAS自旋失败(竞争加剧)。
-
重量级锁(Heavyweight Lock)
- 适用场景:高并发竞争。
- 实现:线程阻塞,依赖操作系统互斥量(mutex)实现同步,开销较大。
4. 同步代码的字节码实现
-
同步代码块:通过
monitorenter和monitorexit指令实现。public void syncBlock() { synchronized (this) { // monitorenter // 代码逻辑 } // monitorexit } -
同步方法:通过方法访问标志
ACC_SYNCHRONIZED实现。public synchronized void syncMethod() { // 代码逻辑 }
5. 优化机制
- 锁消除(Lock Elimination) :JIT编译器检测到不可能存在共享数据竞争时,自动移除锁。
- 锁粗化(Lock Coarsening) :将多个连续的锁操作合并为一个,减少锁开销。
- 自适应自旋(Adaptive Spinning) :根据历史自旋成功率动态调整自旋次数,避免CPU空转。
6. 性能对比
| 锁类型 | 适用场景 | 性能开销 | 实现依赖 |
|---|---|---|---|
| 偏向锁 | 单线程无竞争 | 极低 | CAS |
| 轻量级锁 | 低并发交替执行 | 低 | CAS + 自旋 |
| 重量级锁 | 高并发竞争 | 高 | 操作系统互斥量(mutex) |
总结
synchronized通过对象头、Monitor和锁升级机制实现线程同步。- 锁升级策略(偏向锁→轻量级锁→重量级锁)平衡了性能和安全性。
- 优化机制(锁消除、锁粗化)进一步减少同步开销。
扩展思考:
- 在高并发场景下,为何最终会升级为重量级锁?
synchronized与ReentrantLock有何异同?