synchronized底层原理与锁升级

synchronized底层原理与锁升级

synchronized 是 Java 并发中最常用也最容易被误解的关键字。从早期的重量级锁到现在的锁升级机制,JVM 对它做了很多优化。本文从底层原理到实际使用,把关键细节讲清楚。

synchronized 的三种用法

// 1. 同步实例方法(锁当前对象)
public synchronized void method() { }

// 2. 同步静态方法(锁Class对象)
public static synchronized void staticMethod() { }

// 3. 同步代码块(锁指定对象)
public void block() {
synchronized (this) { }
synchronized (Object.class) { }
}

对象头与 Mark Word

在 HotSpot 虚拟机中,对象在内存中的布局分为三部分:

  1. 对象头(Header):Mark Word + 类型指针
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

#

Mark Word 结构(64位 JVM)

| 锁状态   | 61bit                        | 2bit | 1bit(偏向锁位)|
|----------|------------------------------|------|----------------|
| 无锁 | hashcode(31) + 分代年龄(4) + 0 | 01 | 0 |
| 偏向锁 | 线程ID(54) + 分代年龄(4) + 1 | 01 | 1 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | - |
| 重量级锁 | 指向互斥量(Monitor)的指针 | 10 | - |
| GC标记 | 空 | 11 | - |

synchronized 的字节码

public synchronized void method();
// 字节码:flags: ACC_PUBLIC, ACC_SYNCHRONIZED

public void block();
// 字节码:monitorenter + monitorexit

同步代码块通过 monitorentermonitorexit 指令实现:

code:
0: aload_1
1: dup
2: astore_2
3: monitorenter // 获取锁
4: ... // 同步代码
14: aload_2
15: monitorexit // 释放锁(正常退出)
16: goto 24
19: astore_3 // 异常处理
20: aload_2
21: monitorexit // 释放锁(异常退出)
22: aload_3
23: athrow

注意:编译器会生成两个 monitorexit,确保异常时也能释放锁。

Monitor(管程)

每个 Java 对象关联一个 Monitor,由 ObjectMonitor 实现:

ObjectMonitor() {
_header = NULL;
_count = 0; // 重入次数
_waiters = 0;
_recursions = 0; // 锁重入次数
_object = NULL;
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // 调用wait的线程队列
_EntryList = NULL; // 等待获取锁的线程队列
}

锁升级过程

JDK 6 引入锁升级优化,减少重量级锁的开销:

#

1. 无锁(New)

对象刚创建,没有线程访问。

#

2. 偏向锁(Biased Locking)

场景:只有一个线程访问同步块。

// 开启偏向锁(默认开启)
-XX:+UseBiasedLocking
// 延迟开启(默认4秒)
-XX:BiasedLockingStartupDelay=0

原理:第一次获取锁时,将线程 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)

锁升级流程图

对象创建
|
v
无锁(001)
|
| 第一个线程获取锁
v
偏向锁(101,记录线程ID)
|
|-- 同一线程再次进入:检查线程ID,直接通过
|
|-- 其他线程尝试获取:撤销偏向锁
|
v
轻量级锁(00,CAS替换Mark Word)
|
|-- 获取成功:持有锁执行
|
|-- CAS失败:自旋等待
|
|-- 自旋成功:获取轻量级锁
|
|-- 自旋失败(默认10次):膨胀为重量级锁
|
v
重量级锁(10,Monitor,线程阻塞)

锁优化参数

# 关闭偏向锁
-XX:-UseBiasedLocking

# 设置自旋次数
-XX:PreBlockSpin=10

# 自适应自旋(JDK 6+ 默认开启)
-XX:+UseSpinning

锁消除与锁粗化

#

锁消除(Lock Elimination)

JIT 编译器发现不可能存在竞争时,消除锁:

public void method() {
Object lock = new Object();
synchronized (lock) { // 局部变量,不可能共享,锁被消除
// ...
}
}

开启逃逸分析后自动优化:

-XX:+DoEscapeAnalysis  // 默认开启

#

锁粗化(Lock Coarsening)

将相邻的同步代码块合并:

// 优化前
for (int i = 0; i < 100; i++) {
synchronized (lock) {
// ...
}
}

// 优化后
synchronized (lock) {
for (int i = 0; i < 100; i++) {
// ...
}
}

实际建议

  1. 不要手动关闭偏向锁:除非明确知道有大量竞争
  2. 减少锁的持有时间:只在必要时同步
  3. 减小锁的粒度:使用细粒度锁或分段锁
  4. 避免在锁中调用外部方法:防止死锁和性能问题
// 好的做法:减小锁粒度
public class Counter {
private final Object lock = new Object();
private long count;

public void increment() {
synchronized (lock) {
count++;
} // 尽快释放锁
}
}

总结

锁类型 优点 缺点 适用场景
偏向锁 无竞争时零开销 撤销有开销 单线程访问
轻量级锁 低竞争时性能好 自旋消耗CPU 交替访问
重量级锁 竞争激烈时稳定 线程阻塞开销 高竞争

理解锁升级机制,有助于分析并发性能问题和调优 JVM 参数。

核心要点

  1. synchronized 可以修饰方法或代码块,前者锁对象实例,后者锁指定对象

  2. 锁升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

  3. 锁消除和锁粗化是 JVM 的优化手段

  4. synchronized 保证原子性、可见性和有序性

总结

synchronized 是 Java 并发的基础,理解它的工作机制很重要。在实际项目中,合理使用 synchronized 可以保证线程安全,但也要注意锁的粒度。


   转载规则


《synchronized底层原理与锁升级》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录