Synchronized 能不能禁止指令重排序?
·
synchronized 能否禁止指令重排序
简短回答
是的,synchronized 能够禁止指令重排序。
详细解析
1. synchronized 的内存语义
synchronized 通过以下机制保证内存可见性和禁止指令重排序:
happens-before 原则
解锁操作 happens-before 对同一个锁的加锁操作
这意味着:
- 线程 A 释放锁之前的所有操作,对线程 B 获取锁后都是可见的
- 释放锁之前的指令不会被重排序到释放锁之后
- 获取锁之后的指令不会被重排序到获取锁之前
2. 内存屏障(Memory Barrier)
synchronized 在底层通过内存屏障实现:
// synchronized 的内存屏障示意
synchronized (lock) {
// 进入同步块:LoadLoad + LoadStore 屏障
// 禁止:上面的读/写重排序到下面
// 临界区代码
// 退出同步块:StoreStore + StoreLoad 屏障
// 禁止:下面的读/写重排序到上面
}
3. 四种内存屏障类型
| 屏障类型 | 作用 |
|---|---|
| LoadLoad | 禁止上面的读操作重排序到下面的读操作之前 |
| StoreStore | 禁止上面的写操作重排序到下面的写操作之前 |
| LoadStore | 禁止上面的读操作重排序到下面的写操作之前 |
| StoreLoad | 禁止上面的写操作重排序到下面的读操作之前 |
4. 实际示例分析
示例1:双重检查锁定(DCL)
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 创建对象
}
}
}
return instance;
}
}
为什么需要 volatile?
虽然 synchronized 能禁止重排序,但 new Singleton() 包含三个步骤:
// 1. 分配内存空间
memory = allocate();
// 2. 初始化对象
initObject(memory);
// 3. 将引用指向分配的内存
instance = memory;
问题:步骤2和步骤3可能被重排序为 1→3→2,导致其他线程看到未初始化的对象。
synchronized 的作用范围:
- synchronized 只保证同步块内的指令不会重排序到同步块外
- 但
new Singleton()在同步块内部,其内部的指令重排序仍可能发生
volatile 的作用:
- volatile 的
StoreLoad屏障禁止了 2 和 3 的重排序 - 保证对象完全初始化后才对其他线程可见
示例2:synchronized 禁止重排序的证明
public class ReorderDemo {
private int x = 0;
private int y = 0;
private final Object lock = new Object();
// 线程A
public void writer() {
synchronized (lock) {
x = 1; // 操作1
y = 2; // 操作2
}
// 操作3:在同步块外
System.out.println("writer done");
}
// 线程B
public void reader() {
synchronized (lock) {
// 操作4:在同步块内
int r1 = y; // 操作5
int r2 = x; // 操作6
System.out.println("r1=" + r1 + ", r2=" + r2);
}
}
}
保证:
- 操作1、操作2 不会被重排序到操作3之后
- 操作5、操作6 不会被重排序到操作4之前
- 线程A释放锁前的所有操作(操作1、操作2)对线程B获取锁后可见
5. synchronized 与 volatile 的对比
| 特性 | synchronized | volatile |
|---|---|---|
| 禁止重排序 | ✅ 是(同步块边界) | ✅ 是(读写操作) |
| 内存可见性 | ✅ 是 | ✅ 是 |
| 原子性 | ✅ 是 | ❌ 否 |
| 适用范围 | 代码块/方法 | 单个变量 |
| 性能开销 | 较高 | 较低 |
6. 完整示例:synchronized 的内存语义
public class MemorySemanticDemo {
private int a = 0;
private boolean flag = false;
private final Object lock = new Object();
// 写线程
public void writer() {
synchronized (lock) {
a = 1; // 1
flag = true; // 2
} // 释放锁:插入 StoreStore + StoreLoad 屏障
}
// 读线程
public void reader() {
synchronized (lock) { // 获取锁:插入 LoadLoad + LoadStore 屏障
if (flag) { // 3
int i = a; // 4
System.out.println("i = " + i); // 必然输出 1
}
}
}
}
保证:
- 操作1、操作2 不会被重排序到释放锁之后
- 操作3、操作4 不会被重排序到获取锁之前
- 如果读线程看到
flag = true,则必然能看到a = 1
7. 底层实现(HotSpot JVM)
// HotSpot 中 synchronized 的实现(简化版)
void ObjectSynchronizer::enter(Handle obj, TRAPS) {
// 获取轻量级锁或重量级锁
// 获取锁成功后,插入内存屏障
OrderAccess::acquire(); // LoadLoad + LoadStore 屏障
}
void ObjectSynchronizer::exit(Handle obj, TRAPS) {
// 释放锁前,插入内存屏障
OrderAccess::release(); // StoreStore + StoreLoad 屏障
// 释放锁
}
总结
| 问题 | 答案 |
|---|---|
| 能否禁止指令重排序 | ✅ 能 |
| 禁止范围 | 同步块边界(块内外的重排序) |
| 实现机制 | 内存屏障(Memory Barrier) |
| 内存语义 | happens-before 原则 |
| 是否需要 volatile | 某些场景下仍需要(如 DCL) |
关键点:
- synchronized 能禁止同步块内外的指令重排序
- synchronized 能保证内存可见性
- 但对于同步块内部的复杂操作(如对象创建),可能仍需要 volatile 配合使用
- synchronized 和 volatile 各有适用场景,根据需求选择
理解 synchronized 的内存语义对于编写正确的并发程序非常重要!
更多推荐


所有评论(0)