volatile 与 Atomic
volatile 和 Atomic 原子类是 Java 并发的核心基础。它们经常出现在开关标记、状态刷新、计数器、无锁更新、并发限流等场景里。只会背“volatile 保证可见性,不保证原子性”是不够的,要理解为什么。
如果要系统理解 JMM 可见性、volatile 写读全过程、内存屏障、CAS 自旋、ABA、AtomicReference、LongAdder、商业场景和线上排查,建议继续看:volatile与Atomic全过程原理。
并发里的三个问题
flowchart TD
A["并发问题"] --> B["可见性<br/>一个线程改了值<br/>另一个线程<br/>看不见"]
B --> C["原子性<br/>多个操作被线程<br/>切换打断"]
C --> D["有序性<br/>编译器或 CPU 重排序<br/>导致执行顺序变化"]| 问题 | 示例 |
|---|---|
| 可见性 | 线程 A 把 running 改成 false,线程 B 仍然读到 true |
| 原子性 | 多线程同时执行 count++,结果少加 |
| 有序性 | 对象还没初始化完,引用先被其他线程看到 |
volatile 解决什么
volatile 主要解决可见性和一定程度的有序性问题。
public class StopFlagDemo {
private static volatile boolean running = true;
public static void main(String[] args) throws Exception {
Thread worker = new Thread(() -> {
while (running) {
// 执行任务
}
System.out.println("线程停止");
});
worker.start();
Thread.sleep(1000);
running = false;
}
}如果 running 不加 volatile,工作线程可能一直读取自己工作内存里的旧值,无法及时停止。
volatile 的 happens-before 语义
对一个 volatile 变量的写,happens-before 后续任意线程对这个 volatile 变量的读。这个规则让 volatile 常用于发布状态。
import java.util.Map;
class ConfigHolder {
private Map<String, String> config;
private volatile boolean ready;
public void load() {
config = Map.of("timeout", "3000");
ready = true;
}
public String get(String key) {
if (!ready) {
return null;
}
return config.get(key);
}
}当另一个线程读到 ready == true 后,根据 volatile 规则,它也应该能看到 ready = true 之前写入的 config 引用。
volatile 可见性原理
flowchart TD
A["线程 A 写 volatile 变量"] --> B["写入主内存"]
B --> C["通知其他线程缓存失效"]
D["线程 B 读 volatile 变量"] --> E["从主内存读取最新值"]可以简单理解为:volatile 要求变量的写入对其他线程尽快可见,读取时不能一直拿旧缓存。
更准确地说,volatile 会在编译器和 CPU 层面加入内存屏障语义,限制特定读写重排序,并让写入及时对其他线程可见。你不需要背具体屏障名字,但要理解它不是“让变量变成原子变量”,而是建立可见性和顺序约束。
volatile 为什么不能保证 count++
count++ 看起来是一行,实际包含三步:
flowchart TD
A["读取 count"] --> B["count 加 1"]
B --> C["写回 count"]多个线程同时执行时可能交叉:
public class VolatileNotAtomicDemo {
private static volatile int count = 0;
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
count++;
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(count);
}
}结果通常小于 100000。原因是 volatile 只能保证每次读写尽量看见最新值,但不能把“读、加、写”三步合成不可打断的整体。
volatile 不能替代锁
如果一个业务动作需要同时修改多个变量,volatile 就不够。
class Account {
private volatile int balance;
private volatile int version;
public void add(int money) {
balance += money;
version++;
}
}balance += money 和 version++ 都不是原子操作,而且两个变量之间也没有整体一致性。多个线程同时执行时,可能出现金额和版本不匹配。此时应该使用锁、事务或 CAS 状态对象整体替换。
AtomicInteger
Atomic 原子类基于 CAS 实现无锁更新。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounterDemo {
private static final AtomicInteger COUNT = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
COUNT.incrementAndGet();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(COUNT.get());
}
}CAS 更新流程
flowchart TD
A["读取旧值 old"] --> B["计算新值 new"]
B --> C["比较内存当前值是否仍是 old"]
C --> D["是:写入 new"]
C --> E["否:说明被别人改过"]
E --> ACAS 的思想是:我更新前先确认别人没改过;如果被别人改了,就重新读取再试。
AtomicReference
不仅数字可以原子更新,对象引用也可以。
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<String> status = new AtomicReference<>("INIT");
boolean success = status.compareAndSet("INIT", "RUNNING");
System.out.println(success);
System.out.println(status.get());
}
}适合状态流转、配置热更新、缓存指针切换等场景。
LongAdder
高并发计数时,LongAdder 通常比 AtomicLong 更适合,因为它把竞争分散到多个槽位。
flowchart TD
A["多个线程递增"] --> B["分散到多个 Cell"]
B --> C["读取时汇总 base 和 Cells"]Demo:
import java.util.concurrent.atomic.LongAdder;
public class LongAdderDemo {
private static final LongAdder QPS = new LongAdder();
public static void main(String[] args) {
QPS.increment();
QPS.increment();
System.out.println(QPS.sum());
}
}适合统计 QPS、请求次数、指标计数;如果需要严格的即时值和 CAS 条件更新,还是用 Atomic 类。
volatile、Atomic、锁怎么选
| 场景 | 推荐 |
|---|---|
| 停止标记、配置开关 | volatile |
| 简单计数和状态 CAS | AtomicInteger、AtomicReference |
| 高并发统计 | LongAdder |
| 多个变量需要一起修改 | synchronized 或 Lock |
| 修改过程包含复杂业务逻辑 | 锁或事务控制 |
商业场景 Demo:开关加计数器
import java.util.concurrent.atomic.LongAdder;
public class FeatureMetric {
private volatile boolean enabled = true;
private final LongAdder requestCount = new LongAdder();
public void handle() {
if (!enabled) {
return;
}
requestCount.increment();
// 处理业务
}
public void close() {
enabled = false;
}
public long count() {
return requestCount.sum();
}
}这里 enabled 用 volatile 保证开关变化可见,requestCount 用 LongAdder 支持高并发统计。
常见风险
| 问题 | 后果 | 建议 |
|---|---|---|
| 认为 volatile 能保证所有线程安全 | count++ 丢失更新 | 原子更新用 Atomic 或锁 |
| CAS 自旋过多 | CPU 占用升高 | 高竞争下考虑锁或 LongAdder |
| 多字段分别 Atomic | 状态不一致 | 多字段一致性用锁 |
| 忽略 ABA 问题 | CAS 误判没有变化 | 使用版本号或 AtomicStampedReference |
本章小结
volatile 适合解决可见性和禁止关键重排序,不适合复合操作原子性。Atomic 类通过 CAS 实现无锁原子更新,适合简单状态和计数。并发设计不要只看“能不能无锁”,要看竞争强度、状态一致性和业务复杂度。
