synchronized底层原理与锁升级
synchronized 是 Java 并发中最常用也最容易被误解的关键字。从早期的重量级锁到现在的锁升级机制,JVM 对它做了很多优化。本文从底层原理到实际使用,把关键细节讲清楚。
synchronized 的三种用法
// 1. 同步实例方法(锁当前对象) |
对象头与 Mark Word
在 HotSpot 虚拟机中,对象在内存中的布局分为三部分:
- 对象头(Header):Mark Word + 类型指针
- 实例数据(Instance Data)
- 对齐填充(Padding)
#
Mark Word 结构(64位 JVM)
| 锁状态 | 61bit | 2bit | 1bit(偏向锁位)| |
synchronized 的字节码
public synchronized void method(); |
同步代码块通过 monitorenter 和 monitorexit 指令实现:
code: |
注意:编译器会生成两个 monitorexit,确保异常时也能释放锁。
Monitor(管程)
每个 Java 对象关联一个 Monitor,由 ObjectMonitor 实现:
ObjectMonitor() { |
锁升级过程
JDK 6 引入锁升级优化,减少重量级锁的开销:
#
1. 无锁(New)
对象刚创建,没有线程访问。
#
2. 偏向锁(Biased Locking)
场景:只有一个线程访问同步块。
// 开启偏向锁(默认开启) |
原理:第一次获取锁时,将线程 ID 写入 Mark Word。后续该线程进入同步块,只需检查线程 ID 是否一致,无需 CAS 操作。
无锁(001) -> 偏向锁(101,记录线程ID) |
#
3. 轻量级锁(Lightweight Locking)
场景:多个线程交替访问(无竞争)。
原理:线程在栈帧中创建 Lock Record,用 CAS 将对象头的 Mark Word 替换为指向 Lock Record 的指针。
偏向锁 -> 撤销偏向 -> 轻量级锁(00) |
#
4. 重量级锁(Heavyweight Locking)
场景:多个线程同时竞争。
原理:线程阻塞,进入 EntryList 等待,由操作系统调度。
轻量级锁 -> CAS失败 -> 自旋 -> 失败 -> 重量级锁(10) |
锁升级流程图
对象创建 |
锁优化参数
# 关闭偏向锁 |
锁消除与锁粗化
#
锁消除(Lock Elimination)
JIT 编译器发现不可能存在竞争时,消除锁:
public void method() { |
开启逃逸分析后自动优化:
-XX:+DoEscapeAnalysis // 默认开启 |
#
锁粗化(Lock Coarsening)
将相邻的同步代码块合并:
// 优化前 |
实际建议
- 不要手动关闭偏向锁:除非明确知道有大量竞争
- 减少锁的持有时间:只在必要时同步
- 减小锁的粒度:使用细粒度锁或分段锁
- 避免在锁中调用外部方法:防止死锁和性能问题
// 好的做法:减小锁粒度 |
总结
| 锁类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 无竞争时零开销 | 撤销有开销 | 单线程访问 |
| 轻量级锁 | 低竞争时性能好 | 自旋消耗CPU | 交替访问 |
| 重量级锁 | 竞争激烈时稳定 | 线程阻塞开销 | 高竞争 |
理解锁升级机制,有助于分析并发性能问题和调优 JVM 参数。
核心要点
synchronized 可以修饰方法或代码块,前者锁对象实例,后者锁指定对象
锁升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁消除和锁粗化是 JVM 的优化手段
synchronized 保证原子性、可见性和有序性
总结
synchronized 是 Java 并发的基础,理解它的工作机制很重要。在实际项目中,合理使用 synchronized 可以保证线程安全,但也要注意锁的粒度。