许多开发者误以为synchronized
能完全禁止指令重排序,而实际上它仅能保证线程间的操作顺序,真正禁用重排序需依赖volatile
。
1. synchronized
的有序性保证
synchronized
通过锁机制间接保证多线程操作的顺序性,具体规则基于 Happens-Before 原则:
- 锁的释放(unlock)操作 Happens-Before 后续的锁获取(lock)操作。
这意味着:- 线程A在同步块内对共享变量的修改,对线程B在获取同一锁后的操作完全可见。
- 线程间的操作顺序在逻辑上按锁的获取顺序执行。
示例:线程间可见性与顺序性
public class SynchronizedOrderDemo {
private int value = 0;
private final Object lock = new Object();
// 线程A:写操作
public void write() {
synchronized (lock) {
value = 42; // 写操作
}
}
// 线程B:读操作
public int read() {
synchronized (lock) {
return value; // 保证读到42
}
}
}
执行顺序:
- 线程A先获取锁,写
value = 42
,释放锁。 - 线程B获取锁后,一定能读到
value = 42
。
2. synchronized
无法禁止指令重排序
尽管synchronized
保证了线程间的顺序性,但同步块内部的指令仍可能被重排序(只要不影响单线程执行结果)。这种重排序对单线程透明,但可能导致多线程问题。
示例:同步块内的指令重排序
synchronized (lock) {
int a = 1; // 操作1
int b = 2; // 操作2
}
- 可能的执行顺序:操作2可能在操作1之前执行。
- 单线程结果一致:无论顺序如何,最终
a=1
,b=2
。 - 多线程隐患:若其他线程依赖
a
和b
的写入顺序,可能导致逻辑错误。
1. volatile
的禁止重排序机制
volatile
通过插入内存屏障,直接限制编译器和处理器的指令重排序:
- 写操作:插入 StoreStore屏障和 StoreLoad屏障。
- 读操作:插入 LoadLoad屏障和 LoadStore屏障。
示例:volatile
禁止对象初始化的重排序
// 双重检查锁定(DCL)单例模式
public class Singleton {
private static volatile Singleton instance; // volatile禁止重排序
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 初始化操作
}
}
}
return instance;
}
}
- 无
volatile
的隐患:
对象初始化可能被重排序为:分配内存 → 返回引用 → 初始化对象。其他线程可能拿到未初始化的对象。 volatile
的作用:
强制按顺序执行:分配内存 → 初始化对象 → 返回引用。
三、对比
特性 | synchronized | volatile |
---|---|---|
线程顺序性 | 保证多线程间的操作顺序(通过锁的Happens-Before规则) | 不直接保证多线程顺序,仅确保单变量操作顺序 |
指令重排序 | 允许同步块内部的重排序(单线程语义下) | 禁止与volatile 变量相关的重排序 |
可见性 | 通过锁释放-获取保证所有变量的可见性 | 仅保证volatile 变量的可见性 |
原子性 | 保证同步块内操作的原子性 | 不保证复合操作的原子性(如i++ ) |
性能开销 | 较高(涉及用户态与内核态切换) | 较低(仅内存屏障开销) |
四、常见误区
1. 误区:synchronized
能完全禁止指令重排序
- 真相:
synchronized
仅保证线程间的操作顺序,但同步块内的指令仍可能被重排序(不影响单线程结果即可)。 - 示例:
若同步块内有两个无关变量的赋值,编译器可能调整其顺序。
2. 误区:volatile
可替代锁
- 错误示例:
private volatile int count = 0; public void increment() { count++; // 非原子操作,volatile无法保证线程安全 }
- 正确方案:使用
AtomicInteger
或synchronized
。