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)

关键点

  1. synchronized 能禁止同步块内外的指令重排序
  2. synchronized 能保证内存可见性
  3. 但对于同步块内部的复杂操作(如对象创建),可能仍需要 volatile 配合使用
  4. synchronized 和 volatile 各有适用场景,根据需求选择

理解 synchronized 的内存语义对于编写正确的并发程序非常重要!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐