synchronized 锁升级机制详解
Java中的synchronized
锁会经历一个从无锁到偏向锁,再到轻量级锁,最后到重量级锁的升级过程,这种优化称为锁升级或锁膨胀。以下是完整的锁升级流程和原理分析:
一、锁状态总览
锁状态 | 存储内容 | 适用场景 | 优缺点 |
---|---|---|---|
无锁 | 对象正常状态 | 单线程访问 | 无竞争开销 |
偏向锁 | 线程ID + Epoch(时间戳) | 同一线程重复访问 | 加解锁几乎无代价 |
轻量级锁 | 指向栈中锁记录的指针 | 多线程交替访问 | 自旋消耗CPU |
重量级锁 | 指向互斥量(mutex)的指针 | 多线程竞争激烈 | 线程阻塞唤醒开销大 |
二、锁升级完整流程
三、各阶段详细原理
1. 偏向锁(Biased Locking)
对象头结构:
|-------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|-------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | 01 |
|-------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 |
|-------------------------------------------------------|--------------------|
工作流程:
- 第一个线程访问时,通过CAS将线程ID写入对象头
- 该线程再次进入时只需检查线程ID是否匹配
- 出现竞争时(另一个线程尝试获取锁),撤销偏向锁
JVM参数:
-XX:+UseBiasedLocking # 启用偏向锁(JDK15后默认禁用)
-XX:BiasedLockingStartupDelay=4000 # 延迟启用时间(ms)
2. 轻量级锁(Lightweight Lock)
加锁过程:
- 在当前线程栈帧中创建锁记录(Lock Record)
- 拷贝对象头Mark Word到锁记录(Displaced Mark Word)
- 用CAS将对象头替换为指向锁记录的指针
- 成功则获取锁,失败则自旋或升级
对象头变化:
|-------------------------------------------------------|--------------------|
| Pointer to Lock Record | State |
|-------------------------------------------------------|--------------------|
| 62 bits | 00 | lightweight |
|-------------------------------------------------------|--------------------|
3. 重量级锁(Heavyweight Lock)
实现原理:
- 依赖操作系统互斥量(mutex)实现
- 未获取锁的线程进入阻塞状态
- 通过监视器(Monitor)管理,涉及用户态到内核态的切换
对象头结构:
|-------------------------------------------------------|--------------------|
| Pointer to Heavyweight Monitor | State |
|-------------------------------------------------------|--------------------|
| 62 bits | 10 | heavyweight |
|-------------------------------------------------------|--------------------|
四、关键升级触发条件
升级路径 | 触发条件 |
---|---|
无锁 → 偏向锁 | 第一个线程访问同步块 |
偏向锁 → 轻量级锁 | 第二个线程尝试获取锁(发生竞争) |
轻量级锁 → 重量级锁 | 自旋超过阈值(-XX:PreBlockSpin ,JDK6后自适应自旋)或第三个线程参与竞争 |
五、特殊处理机制
1. 批量重偏向(Bulk Rebias)
场景:当一类对象的偏向锁被多个线程访问,但未真正竞争时
行为:JVM会批量修改这些对象的偏向线程ID(epoch值递增)
阈值:默认20次(-XX:BiasedLockingBulkRebiasThreshold
)
2. 批量撤销(Bulk Revoke)
场景:当一类对象的偏向锁总是发生竞争
行为:JVM会禁用该类的偏向锁功能
阈值:默认40次(-XX:BiasedLockingBulkRevokeThreshold
)
六、锁升级性能影响
锁状态 | 适用场景 | 性能特点 |
---|---|---|
偏向锁 | 单线程重复访问 | 加解锁几乎无开销(仅1次CAS) |
轻量级锁 | 多线程交替访问 | 自旋消耗CPU,但避免线程阻塞 |
重量级锁 | 高并发竞争 | 线程阻塞/唤醒开销大(微秒级) |
七、实战建议
偏向锁优化:
# 对明确存在竞争的类禁用偏向锁 -XX:-UseBiasedLocking
自旋优化:
# 开启自适应自旋(默认开启) -XX:+UseSpinning -XX:PreBlockSpin=10 # 传统自旋次数(JDK6后无效)
监控工具:
# 查看锁竞争情况 jstack <pid> | grep -A 10 "java.lang.Thread.State" # JFR记录锁事件 jcmd <pid> JFR.start duration=60s filename=lock.jfr
八、与ReentrantLock对比
特性 | synchronized | ReentrantLock |
---|---|---|
实现机制 | JVM内置,自动升级 | Java API实现 |
灵活性 | 相对固定 | 可中断、定时获取、公平锁等 |
性能 | JDK6后优化接近ReentrantLock | 高竞争下表现更好 |
锁降级 | 不支持 | 支持(通过ReadWriteLock) |
锁升级机制是JVM对synchronized
性能优化的核心设计,理解这一过程有助于:
- 正确评估同步代码性能
- 合理选择同步方案
- 针对性优化高并发场景