Java 中 synchronized 关键字实现的锁是可升级的 biased lock,从无锁状态开始,根据竞争情况逐步升级:偏向锁(Biased Locking)→ 轻量级锁(Lightweight Locking)→ 重量级锁(Heavyweight Locking)。此设计旨在减少 uncontended 同步的性能开销,同时保证高竞争场景下的正确性。
Mark Word 结构
HotSpot 在对象头(Object Header)中存储锁状态。对象头分为两部分:
| | |
|---|
| | |
| hash code、GC 分代年龄、锁状态、偏向线程 ID、锁记录指针、Monitor 指针等 | |
锁状态由对象头的最后 2 位标识:
64 位 JVM 默认开启 -XX:+UseCompressedOops(压缩普通对象指针),对象头实际占用 96 位(mark word 64 位 + type pointer 32 位)。
锁升级流程
1. 偏向锁(Biased Locking)阶段
触发条件:锁对象第一次被线程获取,且 JVM 开启偏向锁(默认开启,JDK 15+ 默认关闭,可通过 -XX:+UseBiasedLocking 开启)。
流程:
- 对象 Mark Word 为
01(无锁状态),计算对象的 hash code 并存储(调用 System.identityHashCode() 时也会触发) - 线程 A 进入同步块时,CAS 将 Mark Word 的偏向线程 ID 字段设置为当前线程 ID
- CAS 成功:获取偏向锁,Mark Word 仍为
01,但偏向线程 ID 指向线程 A - CAS 失败:表示已有其他线程持有偏向锁,触发偏向锁撤销
- 若已退出同步块,则撤销偏向锁,恢复为无锁状态,等待下次竞争时重新偏向
优化:偏向锁撤销时,可使用批量重偏向(bulk rebias) 和批量撤销(bulk revoke) 机制,减少 CAS 失败带来的开销。
2. 轻量级锁(Lightweight Locking)阶段
触发条件:
流程:
- 每个线程在栈帧中创建 Lock Record(锁记录,也叫 Displaced Mark Word)
- Lock Record 结构:
[lock record pointer | empty | ... ] - 将对象当前 Mark Word 复制到 Lock Record 的
displaced mark word 字段
- 线程使用 CAS 将对象 Mark Word 指向自己的 Lock Record(指针)
- CAS 成功:获取轻量级锁,Mark Word 标志位变为
00
- 默认自旋次数:
-XX:PreBlockSpin(JDK 6 默认 10 次,后续版本自适应调整)
3. 重量级锁(Heavyweight Locking)阶段
触发条件:
- 调用
wait() / notify() 等需要操作系统参与的 API
流程:
- JVM 在堆内存中创建 Monitor 对象(
java.lang.Object 的 monitor 字段) - 将对象 Mark Word 更新为指向 Monitor 的指针,标志位变为
10 - 竞争线程进入 Entry Set(入口等待队列),通过操作系统 mutex 阻塞
- 锁释放时,Monitor 从 Wait Set(等待队列)或 Entry Set 中唤醒一个线程
开销:涉及用户态→内核态切换,性能大幅下降(千年级别开销)。
升级条件总结
| | |
|---|
| | |
| | |
| | |
| -XX:BiasedLockingStartupDelay-XX:BiasedLockingBulkRebiasThreshold-XX:BiasedLockingBulkRevokeThreshold | -XX:PreBlockSpin |
锁降级(Lock Downgrade)
降级场景:重量级锁持有期间,等待线程调用 wait() 进入 Wait Set,此时 Monitor 仍为重量级状态;当持有线程调用 notify() 并退出同步块后,Monitor 会降级为轻量级锁或偏向锁(若只有一个线程重新竞争)。
注意:重量级锁不会直接降级为偏向锁(需先撤销偏向),但会在特定条件下重新偏向。
关键参数调优
| | |
|---|
-XX:+UseBiasedLocking | | |
-XX:BiasedLockingStartupDelay | | |
-XX:BiasedLockingBulkRebiasThreshold | | |
-XX:BiasedLockingBulkRevokeThreshold | | |
-XX:UseSpinning | | |
-XX:PreBlockSpin | | |
-XX:SlowSymantics | | |
最佳实践
- 减小锁粒度:使用细粒度锁(如
ConcurrentHashMap 的分段锁)减少竞争 - 锁分离:读写锁(
ReentrantReadWriteLock)、StampedLock 乐观读 - 使用无锁结构:
AtomicInteger、LongAdder(高并发累加场景) - 合理设置锁超时:
tryLock(timeout) 避免无限等待 - 谨慎使用
synchronized 修饰方法:方法级锁粒度较粗,考虑改为块级锁
面试要点
- 核心:理解偏向锁、轻量级锁、重量级锁三阶段升级的触发条件和性能特征
- Mark Word:掌握对象头的锁标志位和存储内容
- CAS 与自旋:轻量级锁通过 CAS + 自旋实现,降低线程阻塞开销
- Monitor 机制:重量级锁依赖操作系统 mutex,涉及用户态/内核态切换
- 参数调优:能够解释常见 JVM 参数的作用和适用场景
- 锁消除(Lock Elimination):JIT 编译时通过逃逸分析移除不必要的锁
- 锁粗化(Lock Coarsening):JVM 将多个相邻同步块合并为一个
常见问题
Q:synchronized 为什么比 ReentrantLock 慢?A:ReentrantLock 基于 AQS 实现,支持非公平/公平锁、可中断、超时等高级特性;synchronized 是 JVM 内置锁,在 JDK 6 后已优化至与 ReentrantLock 性能相当,但在复杂场景下 ReentrantLock 更灵活。
Q:偏向锁在什么情况下会失效?A:
- 调用
System.identityHashCode(obj) 计算 hash code - 调用
wait() / notify()(需升级为重量级锁)
Q:轻量级锁自旋过多会有什么问题?A:自旋占用 CPU 资源,若长时间无法获取锁,会导致 CPU 利用率飙升,影响其他线程执行。
Q:如何查看锁状态?A:
jstack <pid>:查看线程栈,识别 BLOCKED / WAITING 状态jstat -gc <pid>:监控 GC,间接判断锁竞争-XX:+PrintSafepointStatistics:查看安全点停顿时间(包含锁升级开销)
性能对比(近似值)
数值仅为相对参考,实际性能受 JVM 版本、硬件、锁持有时间等多因素影响。