synchronized 和 volatile 有什么区别?volatile 能保证原子性吗?
简化版
volatile 只保证可见性和有序性(禁止指令重排),不保证原子性;synchronized 三者全保证(可见性、有序性、原子性),因为它是互斥锁。所以 volatile int count; count++ 在多线程下仍会出错(count++ 是「读-改-写」三步,不是原子的)。区别本质:volatile 是轻量的可见性标记(无锁、性能好,但只适合「一个线程写、多个线程读」的状态标志),synchronized 是互斥锁(能保护复合操作,但有锁开销)。
详细版
并发三大特性对照:
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 禁止重排 | ✅ 保证 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 是否加锁 | 否(无阻塞) | 是(互斥、可能阻塞) |
| 修饰对象 | 变量 | 方法、代码块 |
| 性能开销 | 小 | 相对大(虽有锁升级优化) |
| 适用 | 一写多读的状态标志 | 复合操作、临界区保护 |
为什么 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」。