Java Synchronized锁的四种状态与优化机制解析
在Java并发编程中,synchronized
关键字是实现线程同步的核心工具。JVM通过优化形成了四种锁状态:无锁、偏向锁、轻量级锁和重量级锁,这些状态由对象头的Mark Word
管理,并根据竞争强度动态升级。本文将深入分析锁状态、锁升级阈值、Mark Word
的作用,以及高版本JDK为何取消偏向锁的原因,特别聚焦偏向锁的缺陷、轻量级锁、自适应自旋、逃逸分析和锁消除的详细讲解,以应对面试中的深入追问。
一、Synchronized锁的四种状态
1. 无锁状态
无锁状态下,对象未被线程锁定,Mark Word
存储哈希码、GC分代年龄等信息,线程可直接访问对象。
- 适用场景:未被
synchronized
修饰或无竞争。 - Mark Word指向:哈希码或GC信息。
- 特点:无同步开销,性能最高。
2. 偏向锁
偏向锁优化单线程反复访问的场景,Mark Word
记录偏向线程的ID和偏向标志位。线程再次访问时,只需检查线程ID,无需原子操作。
- 适用场景:单线程反复进入同步块。
- Mark Word指向:偏向线程的ID。
- 特点:锁获取成本低,竞争时需撤销。
3. 轻量级锁
轻量级锁通过CAS(Compare-And-Swap)操作实现,适用于低竞争场景。线程在栈帧中创建锁记录,尝试用CAS将Mark Word
替换为指向锁记录的指针。
- 适用场景:多线程交替访问,竞争时间短。
- Mark Word指向:线程栈帧中的锁记录指针。
- 特点:避免操作系统互斥锁,性能较高。
4. 重量级锁
重量级锁依赖操作系统互斥量(Mutex),用于高竞争场景。Mark Word
指向监视器(Monitor)对象,线程通过系统调用管理锁。
- 适用场景:多线程高并发竞争。
- Mark Word指向:Monitor对象。
- 特点:涉及用户态和内核态切换,开销最大。
二、锁升级的阈值分析
锁升级根据竞争强度动态调整,从无锁到偏向锁、轻量级锁、重量级锁。以下是阈值和触发条件:
1. 无锁到偏向锁
- 阈值:对象首次被
synchronized
修饰,且偏向锁启用(JDK 15前默认)。 - 触发条件:单线程进入同步块,
Mark Word
记录线程ID。 - 延迟机制:偏向锁有4秒延迟(
-XX:BiasedLockingStartupDelay
),避免启动时竞争。
2. 偏向锁到轻量级锁
-
阈值:另一线程尝试获取偏向锁,触发撤销,升级为轻量级锁。
-
触发条件:新线程用CAS修改
Mark Word
,线程ID不匹配,需全局安全点撤销。 -
CAS参数:
- 当前值:
Mark Word
当前值(偏向线程ID和标志位)。 - 预期值:新线程期望的
Mark Word
(偏向锁状态)。 - 新值:新线程栈帧中锁记录的指针。
- 当前值:
3. 轻量级锁到重量级锁
- 阈值:自旋次数超过默认值(约10次,
-XX:PreBlockSpin
控制),或竞争线程过多。 - 触发条件:多线程CAS竞争失败次数过多,或自旋时间长,锁膨胀为重量级锁,
Mark Word
指向Monitor。 - 自适应自旋:JVM根据CPU性能和历史自旋成功率调整自旋次数。
三、Mark Word的重要性
Mark Word
是对象头的核心,存储锁状态、哈希码、GC信息等,其指向决定锁类型:
- 无锁:哈希码或GC信息。
- 偏向锁:偏向线程ID。
- 轻量级锁:线程栈帧中的锁记录指针。
- 重量级锁:Monitor对象。
Mark Word
通过CAS动态更新。例如,从偏向锁到轻量级锁,Mark Word
从线程ID变为锁记录指针;到重量级锁时,变为Monitor指针。
竞争示例(偏向锁到轻量级锁)
Object obj = new Object();
synchronized (obj) { // 线程T1获取偏向锁
System.out.println("T1 holds lock");
}
// 线程T2竞争
synchronized (obj) { // T2触发偏向锁撤销
System.out.println("T2 holds lock");
}
-
过程:
- T1获取偏向锁,
Mark Word
记录T1的ID。 - T2尝试用CAS将
Mark Word
替换为T2锁记录指针,当前值(T1的ID)与预期值(偏向锁状态)匹配,但线程ID不同,CAS失败。 - JVM暂停线程(全局安全点),撤销偏向锁,更新
Mark Word
为轻量级锁状态,T2通过CAS获取锁。
- T1获取偏向锁,
轻量级锁竞争示例
Object obj = new Object();
synchronized (obj) { // 线程T1获取轻量级锁
System.out.println("T1 holds lock");
}
// 线程T2竞争
synchronized (obj) { // T2尝试CAS
System.out.println("T2 holds lock");
}
-
过程:
- T1用CAS将
Mark Word
替换为T1锁记录指针。 - T2尝试CAS,当前值(T1锁记录指针)与预期值(无锁或偏向锁)不匹配,CAS失败。
- T2自旋重试,超过阈值后锁膨胀为重量级锁,
Mark Word
指向Monitor。
- T1用CAS将
四、轻量级锁的详细分析
轻量级锁是synchronized
优化的重要部分,旨在减少操作系统互斥锁的开销。
1. 实现机制
-
锁记录:线程在栈帧中创建锁记录(Lock Record),包含
Mark Word
的副本和指向对象的指针。 -
CAS操作:线程尝试用CAS将对象头的
Mark Word
替换为指向锁记录的指针。- 成功:线程获取轻量级锁,
Mark Word
指向锁记录。 - 失败:说明其他线程已持有锁,进入自旋或升级为重量级锁。
- 成功:线程获取轻量级锁,
-
解锁:线程用CAS将
Mark Word
恢复为原始值(从锁记录中获取),释放锁。
2. 适用场景
- 多线程交替访问同步块,但竞争时间短。
- 示例:多个线程轮流访问共享资源,但锁持有时间短暂(如计数器增减)。
3. 优缺点
-
优点:
- 避免操作系统互斥锁,减少用户态到内核态的切换。
- CAS操作高效,适合低竞争场景。
-
缺点:
- 竞争激烈时,自旋会导致CPU空转,浪费资源。
- 自旋失败过多会升级为重量级锁,增加开销。
4. 示例
Object obj = new Object();
synchronized (obj) { // 线程T1尝试获取轻量级锁
// CAS成功,Mark Word指向T1的锁记录
System.out.println("T1 processing");
}
- 如果T2同时竞争,T2的CAS失败,进入自旋。若自旋超阈值,锁升级为重量级锁。
五、自适应自旋的详细分析
自适应自旋是轻量级锁竞争时的优化机制,允许线程在获取锁失败时短暂“自旋”而不是立即阻塞。
1. 实现机制
-
自旋:线程在CAS失败时反复尝试获取锁,避免线程切换的开销。
-
自适应调整:
- JVM根据历史自旋的成功率和CPU性能动态调整自旋次数。
- 如果前几次自旋成功率高,JVM增加自旋次数;否则减少,尽早升级为重量级锁。
- 默认最大自旋次数约为10次(
-XX:PreBlockSpin
控制),但自适应自旋会动态变化。
-
阈值:自旋时间过长或次数过多,锁升级为重量级锁,线程进入Monitor等待队列。
2. 适用场景
- 锁持有时间短,竞争不激烈的场景。
- 示例:多线程竞争共享资源,但锁很快释放(如短暂的同步方法)。
3. 优缺点
-
优点:
- 减少线程上下文切换(用户态到内核态)的开销。
- 自适应调整平衡了CPU消耗和锁获取效率。
-
缺点:
- 竞争激烈时,自旋浪费CPU资源。
- 自旋时间过长可能导致性能下降。
4. 示例
Object obj = new Object();
synchronized (obj) { // T1持有轻量级锁
Thread.sleep(10); // 模拟短暂持有
}
// T2竞争
synchronized (obj) { // T2自旋等待
System.out.println("T2 acquired lock");
}
- T2的CAS失败后自旋,JVM根据T1的持有时间调整自旋次数。若- 问题:偏向锁仅在单线程反复访问同步块时有效,但在现代多核CPU和并发应用(如Web服务器、微服务)中,多线程竞争是常态,单线程场景稀少。
- 影响:偏向锁的优化效果在实际应用中大幅减少,收益无法覆盖其复杂性。
3. 替代优化的成熟
- 问题:轻量级锁、自适应自旋、逃逸分析和锁消除等优化已能高效处理低竞争场景。例如,JIT编译器可通过锁消除移除不必要的同步操作。
- 影响:这些替代方案在多线程环境中更灵活,偏向锁的独特优势不再突出。
4. 复杂性和维护成本
- 问题:偏向锁增加了锁状态管理、撤销逻辑和调试复杂性。撤销操作涉及复杂的安全点机制,容易引发性能问题或错误。
- 影响:移除偏向锁简化了HotSpot虚拟机的实现,降低了开发和维护成本,提高了JVM的稳定性。
5. 实测性能影响微弱
- 问题:OpenJDK社区测试显示,禁用偏向锁后,大多数应用的性能变化可以忽略不计,某些高并发场景甚至因避免撤销开销而性能提升。
- 影响:数据表明偏向锁的收益在现代硬件和应用中已不显著,移除它是基于实证的优化选择。
五、自旋锁中CAS的[当前值/预期值/新值]
在轻量级锁和自旋锁中,CAS是实现原子操作的核心。CAS涉及三个参数:
- 当前值(Actual Value) :内存中
Mark Word
的实际值。 - 预期值(Expected Value) :线程期望的
Mark Word
值(基于读取时的状态)。 - 新值(New Value) :线程希望将
Mark Word
更新为的值(通常是锁记录指针)。
示例(轻量级锁)
-
场景:T1持有轻量级锁,
Mark Word
指向T1锁记录。T2竞争锁。 -
CAS参数:
- 当前值:T1的锁记录指针。
- 预期值:T2期望的无锁或偏向锁状态。
- 新值:T2的锁记录指针.
-
过程:T2执行CAS,发现当前值与预期值不匹配,CAS失败,进入自旋。若自旋超阈值,锁升级为重量级锁。
六、总结
synchronized
的四种锁状态通过Mark Word
动态管理,锁升级由竞争强度和自旋阈值驱动。偏向锁因撤销开销高、适用场景少、替代优化成熟、复杂性高和实测收益低,在高版本JDK中被移除。CAS通过当前值
、预期值
和新值
实现高效同步,支撑轻量级锁的实现。理解Mark Word
和锁升级机制有助于优化Java并发性能。
如果您有更多问题或需要代码示例,欢迎留言!