synchronized 原子性、可见性、有序性全解析(附 volatile 深度对比)
在 Java 多线程开发中,synchronized 和 volatile 是解决并发问题的两大核心工具,但它们的能力边界和适用场景有本质区别。本文将结合可见性、有序性、原子性三大问题,详细拆解两者的底层原理、核心差异与最佳实践。
一、核心结论
- synchronized:可同时解决原子性、可见性、有序性三大问题,通过“独占执行+内存同步+锁规则”实现并发安全,但存在锁竞争开销。
- volatile:可解决可见性、有序性问题,但不保证原子性,是无锁的轻量级方案,仅适用于单变量读写场景。
二、synchronized 解决三大问题的底层原理
1. 解决原子性:临界区“独占执行”
问题根源
多线程下,非原子操作(如 i++)会被拆分为「读值→修改→写回」三步,若线程切换发生在步骤间,会导致最终结果不一致(如 10000 次自增后结果小于 10000)。
解决方式
synchronized 包裹的「临界区代码」同一时间仅允许一个线程执行,其他线程需等待锁释放后才能进入。这让临界区操作成为“不可拆分”的整体,天然具备原子性。
代码示例
// 共享变量
private int count = 0;
// 锁对象(推荐使用专用锁,避免this/类对象的隐式锁冲突)
private final Object lock = new Object();
/**
自增方法:synchronized保证原子性
不加synchronized:并发执行后count < 10000(原子性丢失)
加synchronized:并发执行后count = 10000(原子性保证)
*/
public void increment() {
synchronized (lock) {
count++; // 临界区:独占执行,不可拆分
}
}
2. 解决可见性:强制内存同步(工作内存 ↔ 主内存)
问题根源
线程会将共享变量缓存到「工作内存」,修改后若未及时刷新到「主内存」,其他线程读取的仍是旧值,导致“可见性丢失”(如线程1修改了变量,线程2无法感知)。
解决方式(JMM 内存语义)
- 加锁时:清空当前线程工作内存,强制从主内存重新读取共享变量的最新值;
- 解锁时:将当前线程工作内存中修改的共享变量,强制刷新到主内存。
这两步操作确保了“加锁线程的修改”对“后续加锁线程”实时可见。
代码示例
// 共享开关变量
private boolean run = true;
private final Object lock = new Object();
/**
线程1执行:修改开关变量
解锁时,run=false 会被强制刷入主内存
*/
public void stopLoop() {
synchronized (lock) {
run = false;
}
}
/**
线程2执行:循环读取开关变量
加锁时,强制从主内存读取最新的run值
*/
public void loop() {
while (true) {
synchronized (lock) {
if (!run) {
break; // 能及时感知到run的修改,退出循环
}
}
}
}
3. 解决有序性:锁规则限制重排序
问题根源
JVM 为优化性能会对指令重排序(如“对象赋值”与“对象初始化”顺序调换),单线程下重排序不影响结果,但多线程下会导致执行顺序混乱(如双重检查锁的空指针问题)。
解决方式(JMM 锁规则)
synchronized 依托“锁规则”保证有序性,核心逻辑:
- 持有相同锁的同步块,必须按代码书写顺序串行执行;
- 禁止「临界区内外」的指令重排序,但允许临界区内部指令重排序(单线程执行,不影响结果);
- 本质:所有持有相同锁的线程,看到的临界区执行顺序与代码逻辑完全一致。
代码示例(解决双重检查锁的有序性问题)
// 单例对象(需加volatile阻止初始化重排序,否则DCL仍有风险)
private static volatile Singleton instance;
/**
双重检查锁(DCL):兼顾性能与并发安全
synchronized 保证临界区内外指令不重排序
*/
public static Singleton getInstance() {
// 第一次检查:无锁,提高性能(避免每次获取实例都加锁)
if (instance == null) {
// 加锁:限制串行执行,保证有序性
synchronized (Singleton.class) {
// 第二次检查:防止多线程并发创建多个实例
if (instance == null) {
instance = new Singleton(); // 临界区:禁止与外部代码重排序
}
}
}
return instance;
}
// 注:instance 必须加 volatile,阻止「对象赋值」与「初始化」的重排序,否则仍可能获取到未初始化的对象。具体原因见https://blog.csdn.net/weixin_68315058/article/details/158742384?sharetype=blogdetail&sharerId=158742384&sharerefer=PC&sharesource=weixin_68315058&spm=1011.2480.3001.8118
三、volatile 核心作用:可见性与有序性
1. 可见性:保证所有线程看到的变量值一致
问题根源
Java 内存模型(JMM)中,线程会将共享变量缓存到本地内存(工作内存),修改后若未及时刷新到主内存,其他线程读取的仍是旧值,导致“可见性丢失”。
volatile 可见性实现(JMM 内存语义)
- 写操作:当线程写入 volatile 变量时,JMM 会强制将本地内存中修改的变量值刷新到主内存。
- 读操作:当线程读取 volatile 变量时,JMM 会将本地内存中该变量的缓存置为无效,强制从主内存重新读取最新值。
代码示例:volatile 解决可见性问题
public class VolatileExample {
// 未加 volatile:线程A可能永远看不到stop的变化,陷入死循环
// private static boolean stop = false;
// 加 volatile:保证可见性
private static volatile boolean stop = false;
public static void main(String[] args) {
// 线程A:循环检查stop变量
new Thread("Thread A") {
@Override
public void run() {
while (!stop) {
// 空循环
}
System.out.println("3: " + Thread.currentThread().getName() + " 停止了");
}
}.start();
// 主线程:1秒后修改stop变量
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("1: 主线程等待一秒...");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2: 将stop变量设置为true");
stop = true;
}
}
执行结果:未加 volatile 时,线程A会无限循环;加 volatile 后,线程A能立即感知到 stop 的变化并退出循环。
2. 有序性:通过 happens-before 规则禁止重排序
volatile 有序性实现
volatile 依托 JMM 的 happens-before 规则和内存屏障指令,严格限制指令重排序,保证执行顺序的可预测性:
- happens-before 规则:对一个 volatile 变量的写操作,happens-before 于任意后续对该变量的读操作。
- 内存屏障:JVM 会在 volatile 读写操作前后插入内存屏障,禁止特定类型的处理器重排序,核心规则如下:
| 是否能重排序 | 第二个操作 | ||
|---|---|---|---|
| 第一个操作 | 普通读/写 | volatile读 | volatile写 |
| 普通读/写 | YES | YES | NO |
| volatile读 | NO | NO | NO |
| volatile写 | YES | NO | NO |
代码示例:volatile 保证有序性
class VolatileExample {
int a = 0;
volatile boolean flag = false;
// 线程A执行:先修改a,再修改volatile变量flag
public void writer() {
a = 1; // 1. 线程A修改共享变量a
flag = true; // 2. 线程A修改volatile变量flag
}
// 线程B执行:先读取volatile变量flag,再读取a
public void reader() {
if (flag) { // 3. 线程B读取volatile变量flag
int i = a; // 4. 线程B读取共享变量a
// i 一定等于1,因为happens-before规则保证了有序性
}
}
}
根据 happens-before 规则:1 happens-before 2,2 happens-before 3,3 happens-before 4,因此 1 happens-before 4,线程B读取的a一定是线程A修改后的值。
3. 关键局限:volatile 不保证原子性
volatile 仅能保证单次读写的可见性和有序性,但无法保证复合操作的原子性,这是它与 synchronized 最核心的区别。
反例:volatile 无法保证 i++ 的原子性
public class VolatileAtomicExample {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++; // 非原子操作:读-改-写三步
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// 预期结果:20000,实际结果通常小于20000
System.out.println("count = " + count);
}
}
原因:count++ 是“读取count→修改count→写回count”的复合操作,volatile 仅能保证每次读取和写入的可见性,但无法保证这三步操作的原子性,线程切换仍会导致结果丢失。
四、synchronized vs volatile 核心对比
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | 完全保证(临界区独占执行) | 不保证(仅保证单次读写可见性) |
| 可见性 | 加解锁时强制内存同步 | 写操作刷新主内存,读操作从主内存读取 |
| 有序性 | 禁止临界区内外重排序;块内可重排 | 内存屏障严格限制重排序(无例外) |
| 性能 | 稍差(存在锁竞争/上下文切换开销) | 极好(无锁,仅内存屏障微小开销) |
| 适用场景 | 多步操作(i++、业务逻辑、DCL) | 单变量读写(开关变量、状态标记) |
五、总结与最佳实践
1. 核心总结
- synchronized:全能型方案,通过锁机制同时解决原子性、可见性、有序性,但性能开销较大。
- volatile:轻量级方案,通过内存屏障解决可见性和有序性,但不保证原子性,仅适用于单变量场景。
- 两者互补:简单变量读写用 volatile 提升性能,多步操作用 synchronized 保证原子性,禁止混合同步方式。
2. 最佳实践
- 开关变量、状态标记:优先用 volatile(如
volatile boolean stop)。 - 复合操作(i++、业务逻辑):必须用 synchronized 或原子类(如 AtomicInteger)保证原子性。
- 双重检查锁(DCL):需同时使用 synchronized 和 volatile,synchronized 保证临界区安全,volatile 禁止对象初始化重排序。
- 避免“伪同步”:不要依赖 System.out.println() 等自带锁的方法解决可见性,需显式使用 synchronized 或 volatile。
更多推荐



所有评论(0)