详细聊聊 Synchronized,以及锁的升级过程
在Java中,synchronized
关键字是用于实现线程同步的重要机制,它通过内置锁(Monitor)确保多个线程对共享资源的安全访问。
1. synchronized
的基本使用与实现原理
使用方式
- 修饰实例方法:锁是当前对象实例。
public synchronized void method() { ... }
- 修饰静态方法:锁是当前类的Class对象。
public static synchronized void staticMethod() { ... }
- 同步代码块:需显式指定锁对象(任意对象均可)。
synchronized (lockObject) { ... }
底层实现
- Monitor 机制:每个对象关联一个Monitor(监视器锁),通过
monitorenter
和monitorexit
字节码指令实现加锁/解锁。- 线程进入同步块时,执行
monitorenter
尝试获取锁。 - 退出同步块时,执行
monitorexit
释放锁。
- 线程进入同步块时,执行
2. 对象头与锁状态标记
每个Java对象在内存中分为三部分:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)。对象头中的Mark Word字段记录了锁状态信息。
Mark Word 结构(以64位JVM为例)
锁状态 | 存储内容 |
---|---|
无锁 | 对象的哈希码、分代年龄、是否偏向锁(1 bit) |
偏向锁 | 偏向线程ID、偏向时间戳、分代年龄、锁标志位(01) |
轻量级锁 | 指向线程栈中锁记录(Lock Record)的指针,锁标志位(00) |
重量级锁 | 指向Monitor对象(重量级锁)的指针,锁标志位(10) |
3. 锁的升级过程
JVM根据线程竞争情况动态调整锁状态,以减少性能开销。锁升级的路径为:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
(1) 偏向锁(Biased Locking)
- 适用场景:单线程反复进入同步块,无实际竞争。
- 核心机制:
- 对象首次被线程访问时,将线程ID写入Mark Word,进入偏向模式。
- 后续该线程进入同步块时,无需执行CAS操作,直接检查线程ID是否匹配。
- 优势:消除无竞争时的同步开销。
- 撤销条件:
- 其他线程尝试获取锁时,触发偏向锁撤销。
- 需要等待全局安全点(STW),检查原线程是否存活或已释放锁。
(2) 轻量级锁(Lightweight Locking)
- 适用场景:多线程交替执行同步块,竞争不激烈。
- 核心机制:
- 线程在栈帧中创建锁记录(Lock Record),将对象Mark Word复制到锁记录中。
- 通过CAS将Mark Word替换为指向锁记录的指针。成功则获取锁;失败则膨胀为重量级锁。
- 优势:避免线程阻塞,通过自旋(CAS)减少内核态切换开销。
- 自旋优化:
- 适应性自旋:JVM根据历史自旋成功率动态调整自旋次数。
(3) 重量级锁(Heavyweight Locking)
- 适用场景:多线程高并发竞争。
- 核心机制:
- Monitor对象(C++实现)管理线程竞争,包含
_owner
(持有者)、_EntryList
(阻塞队列)、_WaitSet
(等待队列)。 - 未获取锁的线程进入
_EntryList
,由操作系统调度(涉及用户态到内核态切换)。
- Monitor对象(C++实现)管理线程竞争,包含
- 特点:线程阻塞,响应慢但公平。
4. 锁升级的触发条件
步骤 | 触发条件 |
---|---|
无锁 → 偏向锁 | 对象首次被线程访问,JVM启用偏向锁(默认开启,Java 15后需手动开启)。 |
偏向锁 → 轻量级锁 | 其他线程尝试获取锁,导致偏向锁撤销。 |
轻量级锁 → 重量级锁 | CAS自旋失败(超过阈值或竞争激烈),触发锁膨胀(Inflate)。 |
5. 锁的不可逆性与性能权衡
- 不可逆性:锁升级后无法降级,因为降级会增加复杂性和性能损耗。
- 性能权衡:
- 偏向锁:适合单线程场景,但撤销成本高(需STW)。
- 轻量级锁:适合低竞争场景,依赖CAS自旋。
- 重量级锁:适合高竞争场景,牺牲响应时间保证稳定性。
6. Monitor 的详细结构
Monitor对象(如ObjectMonitor
)包含以下关键字段:
_owner
:当前持有锁的线程。_recursions
:锁的重入次数。_EntryList
:等待获取锁的线程队列。_WaitSet
:调用wait()
后进入等待状态的线程队列。
7. 实际案例:锁升级过程
场景:两个线程交替执行同步块
- 初始状态:对象无锁。
- 线程A进入同步块:升级为偏向锁,Mark Word记录线程A的ID。
- 线程B尝试进入:触发偏向锁撤销,升级为轻量级锁,线程B通过CAS竞争。
- 线程B CAS失败:自旋后仍未成功,升级为重量级锁,线程B进入
_EntryList
阻塞。
8. 最佳实践
- 避免过度同步:减少锁粒度(如使用
ConcurrentHashMap
)。 - 优先使用轻量级工具:如
ReentrantLock
、StampedLock
(需手动管理)。 - 监控锁竞争:通过JVM参数(
-XX:+PrintFlagsFinal
)或工具(Arthas)分析锁状态。