← 返回题目列表

synchronized 和 volatile 有什么区别?volatile 能保证原子性吗?

高频 中等 第 14 / 31 题 更新于 2026/07/26
synchronizedvolatile原子性可见性

简化版

volatile 只保证可见性有序性(禁止指令重排),不保证原子性synchronized 三者全保证(可见性、有序性、原子性),因为它是互斥锁。所以 volatile int count; count++ 在多线程下仍会出错(count++ 是「读-改-写」三步,不是原子的)。区别本质:volatile 是轻量的可见性标记(无锁、性能好,但只适合「一个线程写、多个线程读」的状态标志),synchronized 是互斥锁(能保护复合操作,但有锁开销)。

详细版

并发三大特性对照

特性volatilesynchronized
可见性✅ 保证✅ 保证
有序性✅ 禁止重排✅ 保证
原子性不保证✅ 保证
是否加锁否(无阻塞)是(互斥、可能阻塞)
修饰对象变量方法、代码块
性能开销相对大(虽有锁升级优化)
适用一写多读的状态标志复合操作、临界区保护

为什么 volatile 不保证原子性——count++ 的真相:

volatile int count = 0;
count++;   // 看似一步,实为三步:
// 1. 读 count 到寄存器(read)
// 2. 寄存器 +1(modify)
// 3. 写回 count(write)

volatile 只保证每一步「读到的是最新值、写完立刻可见」,但三步之间可能被其他线程插入:线程 A 读到 5,还没写回,线程 B 也读到 5,两者都算成 6 写回——本该是 7,结果丢了一次更新。

// 正确的计数:要么加锁,要么用原子类
synchronized void inc() { count++; }          // 锁保证三步原子
AtomicInteger count = new AtomicInteger();     // CAS 保证原子
count.incrementAndGet();

⚠️ 经典误区:「加了 volatile 的变量做 ++ 就线程安全了」——错。volatile 只解决「看得见」,解决不了「改的过程被打断」。计数、累加这类复合操作,必须用 synchronized/Lock 或 Atomic 原子类。

完整版教学

一、并发三特性:先分清要保证什么

所有并发问题都能归到三个特性上,先建立框架:

原子性:一个操作要么全做完、要么没做,中间不被打断(如 count++ 的三步不可分割)
可见性:一个线程改了共享变量,别的线程立刻能看到(不读到过期的缓存值)
有序性:代码执行顺序不被重排打乱(至少在需要的地方不被打乱)

volatile 和 synchronized 的区别,本质就是「各自保证了这三个里的哪几个」:volatile 保证可见性 + 有序性,缺原子性;synchronized 三个全包。理解这一点,所有对比题都能推出来,不用死记。

二、volatile 怎么实现可见性

volatile 的可见性靠内存屏障 + 缓存一致性协议

写 volatile 变量:
  写完立刻把值从「工作内存(CPU 缓存)」刷回「主内存」
  → 并让其他 CPU 缓存里这个变量的副本失效
读 volatile 变量:
  不读缓存,强制从主内存重新加载最新值

普通变量的写可能停留在 CPU 缓存里,别的线程读到旧值;volatile 强制「写立即刷主存、读必读主存」,于是任何线程的写都立刻对其他线程可见。这就是它作为「状态标志」好用的原因——一个线程把 volatile boolean stop = true,其他线程的循环立刻能看到并退出。

三、volatile 怎么禁止重排(有序性)

volatile 的第二个作用是禁止指令重排,靠内存屏障

volatile 写之前:插 StoreStore 屏障(前面的写不能排到 volatile 写之后)
volatile 写之后:插 StoreLoad 屏障
volatile 读之后:插 LoadLoad + LoadStore 屏障(后面的读写不能排到 volatile 读之前)

最经典的应用是双重检查锁单例(DCL)

private static volatile Singleton instance;   // 必须 volatile
// instance = new Singleton() 分三步:分配内存→初始化→赋引用
// 无 volatile 时"初始化"和"赋引用"可能重排,别的线程拿到未初始化完的对象
// volatile 禁止这个重排,保证拿到的 instance 一定是初始化完整的

这里 volatile 的有序性是关键——它保证「对象完全构造好」这件事发生在「引用被赋值」之前,别的线程不会看到半成品。

四、核心分水岭:为什么 volatile 没有原子性

这是本题最核心的追问。可见性和原子性是两码事:

可见性:保证你读到的是"最新的值"
原子性:保证你的"读-改-写"整个过程不被打断

count++ 需要的是原子性,volatile 只给了可见性:
时刻   线程A            线程B            count(主存)
 t1    读 count=5                          5
 t2                    读 count=5          5   ← B 也读到最新的5(可见性OK)
 t3    算 5+1=6                            5
 t4                    算 5+1=6            5
 t5    写回 6                              6
 t6                    写回 6              6   ← 两次++只加了1!丢更新

看到了吗?每一步读到的都是「最新值」(可见性没问题),但两个线程的「读-改-写」交错了,导致一次自增丢失。可见性解决「读到旧值」,但解决不了「改的过程中途被插队」——后者需要原子性,只能靠锁或 CAS。这就是 volatile 的能力边界。

五、synchronized 为什么三者全保证

synchronized 是互斥锁,它的保证更强:

原子性:同一时刻只有一个线程能进临界区,临界区内的操作对其他线程"不可分割"
可见性:解锁时把工作内存刷回主存,加锁时从主存重新读(happens-before 锁规则)
有序性:临界区内虽可能重排,但因为互斥,别的线程看不到中间态,等价于有序

代价是互斥带来的阻塞和上下文切换开销(虽然 JDK 6 后有偏向锁、轻量级锁、自旋等优化,无竞争时开销已很小)。所以选择原则是:只需要「一个线程写、其他线程读」一个标志位,用 volatile(无锁、轻量);涉及「读-改-写」复合操作或保护多个变量的一致性,用 synchronized/Lock

六、选型决策与组合使用

场景选择理由
状态标志(stop、初始化完成)volatile一写多读,只需可见性,无锁最快
DCL 单例的 instance 字段volatile需要禁止重排(有序性)
计数器、累加AtomicXxx 或 synchronized需要原子性
保护多个变量的一致性synchronized/Lock需要临界区互斥
高并发只读多写少读写锁 / CAS更细粒度

三者不是对立的,常组合:如 volatile 保证标志可见,Atomic 保证计数原子,synchronized 保证复合逻辑。一个记忆锚点:volatile 管「看得见」,synchronized/Atomic 管「改得对」

记忆钩子:「volatile = 可见性 + 有序性,无原子性;synchronized = 三者全包但要加锁。count++ 有 volatile 也不安全,得用锁或 Atomic」

七、常见误区与追问

  • 误区:volatile 能保证 count++ 线程安全。 不能,count++ 是读-改-写三步复合操作,volatile 只保证可见性不保证原子性;要用 AtomicInteger 或 synchronized。
  • 误区:volatile 和 synchronized 只是性能差异。 本质是保证的特性不同——volatile 无原子性,能力边界完全不同,不是「弱一点的锁」。
  • 误区:synchronized 不保证可见性。 保证——解锁时刷主存、加锁时读主存(happens-before 锁规则),可见性和 volatile 一样强。
  • 误区:volatile 修饰的引用,其指向对象的字段也是 volatile 的。 不是,volatile 只作用于被修饰的变量本身(引用),对象内部字段不受影响。
  • 追问:volatile 适合什么场景? 「一个线程写、多个线程读」的状态标志(如 stop、ready),以及 DCL 单例字段(需禁止重排);不适合有「读-改-写」的复合操作。
  • 追问:DCL 单例为什么必须给 instance 加 volatile? new 对象分「分配内存-初始化-赋引用」三步,无 volatile 时后两步可能重排,别的线程会拿到未初始化完的半成品;volatile 禁止该重排。
  • 追问:既然 synchronized 更强,为什么还用 volatile? volatile 无锁、无阻塞、开销小,对「只需可见性」的状态标志更高效;用 synchronized 是杀鸡用牛刀且有锁开销。

八、加强记忆

把 volatile 和 synchronized 的区别锚定在并发三特性上:volatile 保证可见性(写立即刷主存、读必读主存)+ 有序性(内存屏障禁止重排),但不保证原子性synchronized 三者全保证,因为它是互斥锁。最核心的考点是「volatile 为什么没有原子性」——count++ 是「读-改-写」三步,volatile 让每步都读到最新值(可见性OK),但三步之间会被其他线程插队导致丢更新,原子性得靠锁或 CAS。选型口诀:「一个线程写、多个线程读」的状态标志用 volatile(无锁轻量)、DCL 单例字段用 volatile(禁重排);复合操作/计数用 AtomicXxx 或 synchronized;保护多变量一致性用 synchronized/Lock。一句话「volatile 管看得见(可见+有序无原子)、synchronized 管改得对(三者全包但加锁),count++ 得靠锁或 Atomic」。