Java并发-synchronized实现原理

Synchronized的实现原理

synchronized 是 Java 并发编程的基石,理解其底层原理对于编写高性能并发程序至关重要。

一、核心概念

synchronized 实现原理依赖于 JVM 的 Monitor(监视器锁)和 对象头(Object Header)。当 synchronized 修饰在方法或代码块上时,会对特定的对象或类加锁,从而确保同一时刻只有一个线程能执行加锁的代码块。

1.1 synchronized 的三种使用方式

使用方式锁对象示例
修饰实例方法当前实例对象 thispublic synchronized void method()
修饰静态方法当前类的 Class 对象public static synchronized void method()
修饰代码块指定的锁对象synchronized(lock) { }

底层字节码实现差异:

1
2
3
4
5
6
7
8
9
10
11
12
// 1. synchronized 修饰方法
public synchronized void syncMethod() {
// 方法的 access_flags 会增加 ACC_SYNCHRONIZED 标志
// JVM 在方法调用时检查该标志,若有则需先获取监视器锁
}

// 2. synchronized 修饰代码块
public void syncBlock() {
synchronized (this) {
// 编译后会在代码块前后插入 monitorenter 和 monitorexit 指令
}
}

通过 javap -v 反编译可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 代码块方式的字节码
0: aload_0
1: dup
2: astore_1
3: monitorenter // 获取锁
4: // 同步代码
7: aload_1
8: monitorexit // 正常退出释放锁
9: goto 17
12: astore_2
13: aload_1
14: monitorexit // 异常退出也会释放锁(保证锁一定被释放)
15: aload_2
16: athrow
17: return

注意:编译器会自动生成两个 monitorexit 指令,一个用于正常退出,一个用于异常退出,确保锁一定会被释放,避免死锁。

二、对象头(Object Header)深度解析

在 HotSpot JVM 中,每个对象的内存布局由三部分组成:

1
2
3
4
5
6
7
8
9
10
11
|----------------------------------------------|
| Object Header (对象头) |
|----------------------------------------------|
| Mark Word (标记字段) | 64 bits / 32 bits |
| Klass Pointer (类型指针) | 64 bits / 32 bits |
| Array Length (数组长度) | 仅数组对象有 |
|----------------------------------------------|
| Instance Data (实例数据) |
|----------------------------------------------|
| Padding (对齐填充) |
|----------------------------------------------|

2.1 Mark Word 详解

Mark Word 是实现 synchronized 的关键,它会根据锁的状态动态存储不同的信息。以 64 位 JVM 为例:

锁状态存储内容标志位
无锁对象 HashCode、GC 分代年龄01
偏向锁偏向线程 ID、偏向时间戳、GC 分代年龄01
轻量级锁指向栈中锁记录的指针00
重量级锁指向 Monitor 对象的指针10
GC 标记11
1
2
3
4
5
6
7
8
9
10
11
|-------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|-------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | 无锁 |
|-------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | 偏向锁 |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | 轻量级锁 |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | 重量级锁 |
|-------------------------------------------------------|--------------------|

三、Monitor(监视器锁)机制

3.1 Monitor 结构

每个 Java 对象都可以关联一个 Monitor 对象。在 HotSpot 虚拟机中,Monitor 是由 ObjectMonitor 实现的(C++ 代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// hotspot/src/share/vm/runtime/objectMonitor.hpp
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录重入次数
_waiters = 0;
_recursions = 0; // 锁的重入次数
_object = NULL;
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // 等待队列(调用 wait 方法的线程)
_WaitSetLock = 0;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL;
FreeNext = NULL;
_EntryList = NULL; // 阻塞队列(等待获取锁的线程)
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread = 0;
}

3.2 Monitor 工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                    +------------------+
| EntryList | 阻塞队列:等待获取锁的线程
+--------+---------+
|
v
+----------+ +------+------+ +-----------+
| Thread | ------> | Owner | ------> | 释放锁 |
+----------+ 获取锁 +------+------+ 执行完 +-----------+
|
| wait()
v
+--------+---------+
| WaitSet | 等待队列:调用 wait 的线程
+------------------+
|
| notify()/notifyAll()
v
+--------+---------+
| EntryList | 重新进入阻塞队列竞争锁
+------------------+

核心流程:

  1. 线程尝试获取锁时,先通过 CAS 设置 _owner 为当前线程
  2. 若 CAS 失败,进入 _EntryList 阻塞队列等待
  3. 持有锁的线程调用 wait() 后释放锁,进入 _WaitSet 等待被唤醒
  4. notify()/notifyAll() 唤醒后,重新进入 _EntryList 竞争锁

四、锁升级机制详解

JDK 1.6 开始,为了减少获取锁和释放锁带来的性能消耗,引入了 偏向锁轻量级锁,锁共有四种状态:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

重要:锁只能单向升级,不能降级(GC 时除外)。这是为了避免频繁升降级带来的性能开销。

4.1 偏向锁(Biased Locking)

设计思想:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁就是为了让线程在无竞争场景下,消除同步原语,进一步提高性能。

加锁过程:

  1. 当线程第一次访问同步块时,检查 Mark Word 中是否存储了当前线程 ID
  2. 如果没有,通过 CAS 将当前线程 ID 写入 Mark Word
  3. 成功后,后续该线程进入同步块只需检查线程 ID 是否匹配,无需 CAS

偏向锁撤销:

  • 当有另一个线程尝试竞争偏向锁时,持有偏向锁的线程会被挂起
  • JVM 在安全点(Safe Point)暂停该线程,检查其是否仍处于同步块中
  • 若已退出同步块,则撤销偏向锁,恢复到无锁状态
  • 若仍在同步块中,则升级为轻量级锁
1
2
3
4
// 可通过 JVM 参数控制偏向锁
-XX:+UseBiasedLocking // 开启偏向锁(JDK 15 之前默认开启)
-XX:BiasedLockingStartupDelay=0 // 偏向锁启动延迟(默认 4 秒)
-XX:-UseBiasedLocking // 关闭偏向锁

注意:JDK 15 开始,偏向锁默认禁用,JDK 18 彻底移除。因为现代 JVM 的 CAS 操作已经足够高效。

4.2 轻量级锁(Lightweight Locking)

设计思想:适用于多个线程交替执行同步块,但不存在竞争的场景。通过 CAS 和自旋来避免线程阻塞。

加锁过程:

  1. JVM 在当前线程的栈帧中创建 Lock Record(锁记录)
  2. 将对象头的 Mark Word 复制到 Lock Record 中(称为 Displaced Mark Word)
  3. 使用 CAS 将对象头的 Mark Word 替换为指向 Lock Record 的指针
  4. CAS 成功,获得轻量级锁;CAS 失败,进入自旋或膨胀为重量级锁
1
2
3
4
5
6
7
栈帧                              对象头
+------------------+ +------------------+
| Lock Record | <--------- | Mark Word |
|------------------| | (ptr to record) |
| Displaced Mark | +------------------+
| Word (原始MW) |
+------------------+

自旋优化:

1
2
3
// 自适应自旋:根据历史成功率动态调整自旋次数
-XX:PreBlockSpin=10 // 自旋次数(已废弃)
// JDK 6 引入自适应自旋,由 JVM 根据运行时数据自动调整

4.3 重量级锁(Heavyweight Locking)

当自旋超过阈值或竞争激烈时,轻量级锁膨胀为重量级锁。此时会使用操作系统的 互斥量(Mutex) 实现线程阻塞和唤醒。

性能开销:涉及用户态到内核态的切换,开销较大(约 1-10 微秒)。

4.4 锁升级流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
                         首次访问同步块
|
v
+------------------------+
| 检查 Mark Word 状态 |
+-----------+------------+
|
+-------------------+-------------------+
| |
v v
无锁/匿名偏向 已偏向其他线程
| |
v v
CAS 设置线程 ID 到达安全点,撤销偏向锁
| |
+-------+-------+ |
| | |
成功 失败 v
| | 升级为轻量级锁
v v |
偏向锁 升级为轻量级锁 <---------------------+
|
+-------+-------+
| |
CAS 成功 CAS 失败
| |
v v
轻量级锁 自旋重试
|
+-------+-------+
| |
自旋成功 自旋失败
| |
v v
轻量级锁 膨胀为重量级锁

五、Synchronized 的可重入性

synchronized 是可重入锁,同一线程可以多次获取同一把锁而不会死锁。

实现原理:Monitor 中的 _recursions 计数器记录重入次数:

  • 每次获取锁:计数器 +1
  • 每次释放锁:计数器 -1
  • 计数器归零时:真正释放锁
1
2
3
4
5
6
7
8
9
10
public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("methodA");
methodB(); // 可以直接调用,不会死锁
}

public synchronized void methodB() {
System.out.println("methodB");
}
}

六、synchronized 与 ReentrantLock 对比

特性synchronizedReentrantLock
实现层面JVM 内置(字节码指令)JDK 实现(AQS)
锁获取方式隐式获取释放显式 lock()/unlock()
可中断不支持支持 lockInterruptibly()
超时获取不支持支持 tryLock(timeout)
公平锁非公平支持公平/非公平
条件变量单一(wait/notify)多个 Condition
锁绑定对象锁对象Lock 实例

七、实战建议

  1. 优先使用 synchronized:代码简洁,JVM 会自动优化,不易出错
  2. 需要高级特性时用 ReentrantLock:如可中断、超时、公平锁、多条件变量
  3. 减小锁粒度:只对必要的代码块加锁,避免持锁时间过长
  4. 避免嵌套锁:容易产生死锁,如必须嵌套则保持获取锁的顺序一致
1
2
3
4
5
6
7
8
9
10
11
12
13
// 推荐:缩小锁范围
public void goodPractice() {
// 非同步操作
doSomething();

synchronized (this) {
// 仅对共享资源操作加锁
updateSharedResource();
}

// 非同步操作
doOtherThing();
}