Skip to content

volatile 与 Atomic 全过程原理 ​

volatile、Atomic 原子类、CAS 是 Java 并发里的基础骨架。很多面试回答只背一句“volatile 保证可见性,不保证原子性”,但真正写生产代码时,你必须知道:它为什么能让别的线程看见,为什么 count++ 仍然会错,Atomic 为什么能原子更新,CAS 为什么会自旋,什么时候不用无锁反而更稳。

学习目标 ​

目标你要能说明白
并发三大问题可见性、原子性、有序性分别是什么
volatile 原理volatile 写和读如何建立 happens-before
内存屏障为什么 volatile 能限制重排序和刷新可见性
原子性边界为什么 volatile 读写单次可见,但 count++ 不是原子操作
CAS 原理Compare And Swap 的比较和交换为什么必须不可拆分
Atomic 原子类AtomicInteger、AtomicReference、AtomicStampedReference、LongAdder 怎么选
商业场景开关、状态流转、指标统计、配置发布怎么落地
线上排查CPU 高、自旋重试、计数不准、状态错乱怎么分析
面试闭环标准回答能跳回原理页解释为什么

先建立三件事 ​

并发问题不是一个问题,而是三类问题。

mermaid
flowchart TD
    A["多个线程访问共享数据"] --> B["可见性问题"]
    A --> C["原子性问题"]
    A --> D["有序性问题"]
    B --> E["一个线程改了值,另一个线程看不到"]
    C --> F["一个动作由多步组成,中途被打断"]
    D --> G["编译器或 CPU 为优化改变执行顺序"]
问题零基础解释典型错误
可见性A 改了变量,B 还在看旧副本普通 boolean running 停不下来
原子性一个操作不能被拆开才安全多线程 count++ 丢失更新
有序性实际执行顺序可能和代码顺序不同双重检查单例暴露半初始化对象

volatile 主要解决可见性和有序性,不解决复合操作原子性。Atomic 主要解决简单变量的原子更新。

JMM 视角:线程为什么会看不到最新值 ​

Java 内存模型可以简化理解成:

mermaid
flowchart TD
    A["主内存中的共享变量"] --> B["线程 A 工作内存"]
    A --> C["线程 B 工作内存"]
    B --> D["线程 A 修改副本"]
    D --> E["写回主内存的时机不确定"]
    C --> F["线程 B 可能继续读旧副本"]

这里的“工作内存”不是 JVM 运行时数据区里某个具体区域,而是 JMM 为了描述多线程可见性抽象出来的概念。真实硬件上可能对应寄存器、CPU 缓存、写缓冲、编译器优化后的临时值。

如果没有明确规则,下面代码可能停不下来:

java
public class StopFlagWrong {
    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(() -> {
            while (running) {
                // busy work
            }
            System.out.println("stopped");
        });

        worker.start();
        Thread.sleep(1000);
        running = false;
    }
}

问题不是主线程没改,而是工作线程没有可靠规则保证一定重新读取到主线程改过的值。

volatile 做了什么 ​

java
private static volatile boolean running = true;

volatile 的核心语义:

能力含义
可见性一个线程写 volatile 变量后,其他线程后续读这个 volatile 变量能看到新值
有序性volatile 写之前的普通写,不能被重排到 volatile 写之后;volatile 读之后的普通读,不能被重排到 volatile 读之前
单次读写原子性对 volatile 变量的一次读或一次写本身是原子的
不保证复合原子性count++ 这种读、改、写三步不安全

流程:

mermaid
flowchart TD
    A["线程 A 修改普通变量 x"] --> B["线程 A 写 volatile flag"]
    B --> C["volatile 写建立发布语义"]
    C --> D["线程 B 读 volatile flag"]
    D --> E["volatile 读建立获取语义"]
    E --> F["线程 B 能看到 A 写 flag 前的普通变量修改"]

标准规则:

text
对一个 volatile 变量的写,happens-before 后续任意线程对这个 volatile 变量的读。

它不是说“时间上 A 先写,B 后读就行”,而是说:如果 B 读到了 A 写入的 volatile 值,那么 A 在写 volatile 之前的结果,也必须对 B 可见。

内存屏障是什么 ​

为了实现 volatile 语义,JVM 和底层 CPU 会使用内存屏障。你可以把内存屏障理解为一类约束:

屏障作用解决什么
禁止特定重排序不让关键读写被优化到错误位置
刷新写入让写入更快对其他线程可见
重新读取不让线程一直使用旧缓存或旧寄存器值

不用死背每个平台的指令,但要知道 volatile 的真实目的不是“每次都从内存慢慢读一下”这么简单,而是通过 JMM 规则和底层屏障,让不同平台上都能得到一致的并发语义。

volatile 写读全过程 ​

以配置发布为例:

java
import java.util.Map;

public class ConfigPublish {
    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);
    }
}

流程:

mermaid
flowchart TD
    A["线程 A 创建 config"] --> B["线程 A 写普通字段 config"]
    B --> C["线程 A 写 volatile ready=true"]
    C --> D["线程 B 读 volatile ready"]
    D --> E{"ready 是否为 true"}
    E -->|是| F["B 读取 config"]
    E -->|否| G["配置未就绪"]

为什么读到 ready == true 后可以读 config?

因为 config = ... 在 ready = true 之前,volatile 写读建立 happens-before,所以线程 B 读到 ready == true 后,能看到 A 写 ready 之前的 config 写入。

如果 ready 不是 volatile,线程 B 可能看到 ready == true 却看不到最新 config,也可能因为重排序导致发布顺序出问题。

volatile 为什么不能保证 count++ ​

count++ 不是一个操作,而是三个动作:

mermaid
flowchart TD
    A["读取 count 当前值"] --> B["在当前值基础上加 1"]
    B --> C["把新值写回 count"]

两个线程同时执行:

text
count 初始值 = 0

线程 A 读到 0
线程 B 读到 0
线程 A 计算 1
线程 B 计算 1
线程 A 写回 1
线程 B 写回 1

执行了两次 ++,结果却是 1

Demo:

java
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 保证“每次读写的可见性”,但不能把读、加、写三步合成一个不可打断的整体。

CAS 是什么 ​

CAS 是 Compare And Swap,比较并交换。它依赖 CPU 原子指令保证下面动作不可拆分:

text
如果内存当前值 == 我预期的旧值,就改成新值;
否则说明别人改过,本次失败。

流程:

mermaid
flowchart TD
    A["读取旧值 old"] --> B["根据 old 计算 new"]
    B --> C["CAS 比较内存当前值"]
    C --> D{"当前值是否仍等于 old"}
    D -->|是| E["写入 new,更新成功"]
    D -->|否| F["说明被其他线程改过"]
    F --> G["重新读取并重试"]
    G --> A

CAS 的优势是:线程失败后不一定进入阻塞,可以重新尝试,所以低竞争、短逻辑的场景性能很好。

CAS 的代价是:竞争激烈时会不断失败重试,CPU 可能很高。

AtomicInteger 怎么工作 ​

java
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());
    }
}

incrementAndGet() 可以理解成:

java
for (;;) {
    int oldValue = get();
    int newValue = oldValue + 1;
    if (compareAndSet(oldValue, newValue)) {
        return newValue;
    }
}

真实源码会使用 JVM 提供的底层能力,比如 Unsafe 或 VarHandle。核心思想不变:读取旧值、计算新值、CAS 更新,失败就重试。

AtomicReference:对象整体替换 ​

多个字段要一起保持一致时,不要把每个字段都做成一个 Atomic,然后分别更新。更稳的方式是封装成不可变对象,用 AtomicReference 整体替换。

java
import java.util.concurrent.atomic.AtomicReference;

public class RuleConfigHolder {
    private static class RuleConfig {
        final int version;
        final String mode;
        final int timeoutMs;

        RuleConfig(int version, String mode, int timeoutMs) {
            this.version = version;
            this.mode = mode;
            this.timeoutMs = timeoutMs;
        }
    }

    private final AtomicReference<RuleConfig> ref =
            new AtomicReference<>(new RuleConfig(1, "NORMAL", 3000));

    public boolean update(int oldVersion, String mode, int timeoutMs) {
        RuleConfig oldConfig = ref.get();
        if (oldConfig.version != oldVersion) {
            return false;
        }
        RuleConfig newConfig = new RuleConfig(oldVersion + 1, mode, timeoutMs);
        return ref.compareAndSet(oldConfig, newConfig);
    }

    public String currentMode() {
        return ref.get().mode;
    }
}

这样可以保证读线程看到的永远是一份完整配置,不会看到 mode 已经更新、timeoutMs 还是旧值的半成品。

ABA 问题 ​

CAS 只比较“当前值是不是旧值”,它不知道中间是否发生过变化。

mermaid
sequenceDiagram
    participant T1 as 线程1
    participant T2 as 线程2
    participant V as 共享变量
    T1->>V: 读取 A
    T2->>V: A 改成 B
    T2->>V: B 又改回 A
    T1->>V: CAS 发现还是 A,更新成功

线程 1 以为“没变化”,但实际上中间经历了 A -> B -> A。

解决思路:

方案原理Java 工具
加版本号值相同也要比较版本是否变过AtomicStampedReference
加标记位比较引用和布尔标记AtomicMarkableReference
不用 CAS 做复杂状态用锁或事务保证完整流程synchronized、Lock、数据库事务

Demo:

java
import java.util.concurrent.atomic.AtomicStampedReference;

public class AbaFixedDemo {
    public static void main(String[] args) {
        AtomicStampedReference<String> ref =
                new AtomicStampedReference<>("A", 1);

        int[] stampHolder = new int[1];
        String oldValue = ref.get(stampHolder);
        int oldStamp = stampHolder[0];

        ref.compareAndSet("A", "B", 1, 2);
        ref.compareAndSet("B", "A", 2, 3);

        boolean success = ref.compareAndSet(oldValue, "C", oldStamp, oldStamp + 1);
        System.out.println(success); // false
    }
}

虽然值又变回了 A,但版本已经从 1 变成 3,所以旧 CAS 失败。

LongAdder 为什么高并发计数更快 ​

AtomicLong 所有线程竞争一个值:

mermaid
flowchart TD
    A["线程1"] --> D["同一个 AtomicLong value"]
    B["线程2"] --> D
    C["线程3"] --> D
    D --> E["竞争激烈时 CAS 频繁失败"]

LongAdder 把竞争拆散:

mermaid
flowchart TD
    A["线程1"] --> D["Cell 0"]
    B["线程2"] --> E["Cell 1"]
    C["线程3"] --> F["Cell 2"]
    D --> G["sum 时汇总"]
    E --> G
    F --> G

它适合高并发统计,比如 QPS、接口调用次数、限流指标。

java
import java.util.concurrent.atomic.LongAdder;

public class MetricCounter {
    private final LongAdder requestCount = new LongAdder();

    public void record() {
        requestCount.increment();
    }

    public long total() {
        return requestCount.sum();
    }
}

注意:LongAdder.sum() 不是强一致快照。统计指标一般可以接受短暂误差;如果你要用“当前值必须严格等于某个值才更新”的条件判断,应该使用 Atomic 或锁。

volatile、Atomic、LongAdder、锁怎么选 ​

场景推荐原因
停止标记volatile boolean只需要可见性
配置是否加载完成volatile ready需要发布语义
简单计数,竞争不高AtomicInteger / AtomicLongCAS 简单直接
高并发指标统计LongAdder分散热点,吞吐高
状态流转AtomicReference 或 AtomicIntegerCAS 控制从旧状态到新状态
多字段一致性锁或不可变对象 + AtomicReference避免半更新
复杂业务流程synchronized / Lock / 事务可读性和一致性更重要

不要迷信“无锁一定更快”。无锁适合逻辑短、冲突低、状态简单的场景。冲突高或业务复杂时,CAS 自旋会让 CPU 空转,代码也会变难维护。

商业场景一:功能开关与指标统计 ​

java
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 分散竞争
handle读开关后处理,关闭后允许少量已进入请求继续完成

如果要求关闭瞬间绝对没有任何请求继续执行,只用 volatile 不够,需要锁、网关下线、流量切断或状态机控制。

商业场景二:订单状态 CAS 流转 ​

订单状态必须按规则流转,例如只能从 WAIT_PAY 到 PAID。

java
import java.util.concurrent.atomic.AtomicReference;

public class OrderStateMachine {
    enum Status {
        WAIT_PAY, PAID, CLOSED
    }

    private final AtomicReference<Status> status =
            new AtomicReference<>(Status.WAIT_PAY);

    public boolean pay() {
        return status.compareAndSet(Status.WAIT_PAY, Status.PAID);
    }

    public boolean close() {
        return status.compareAndSet(Status.WAIT_PAY, Status.CLOSED);
    }

    public Status current() {
        return status.get();
    }
}

这个 demo 适合本地内存状态流转。真实订单系统不能只靠 JVM 内存 Atomic,因为订单状态在数据库里,还要考虑数据库事务、幂等、分布式锁或乐观锁版本号。

线上排查 ​

count++ 统计不准 ​

排查顺序:

mermaid
flowchart TD
    A["统计结果少了"] --> B["检查是否多线程写普通 int/long"]
    B --> C{"是否使用 count++"}
    C -->|是| D["改 Atomic 或 LongAdder"]
    C -->|否| E["检查是否多节点统计未聚合"]
    E --> F["检查异步丢任务、异常吞掉、重复覆盖"]

CPU 高但线程没阻塞 ​

如果线程栈显示大量线程在 Atomic 更新、CAS 循环、无锁队列自旋,说明可能是高竞争自旋。

处理思路:

现象处理
AtomicLong 热点计数 CPU 高改 LongAdder
CAS 状态更新失败率高降低并发、分片、改锁
自旋逻辑里有复杂计算不要用 CAS 包复杂业务,改锁或队列串行化
线上状态偶发错乱检查是否多个 Atomic 字段分别更新导致不一致

volatile 开关不生效 ​

先确认:

检查点说明
是否所有线程读同一个字段多实例、多 ClassLoader、多节点都可能不是同一个变量
字段是否真的加了 volatile只给局部变量或错误字段加没用
线程是否卡在阻塞调用里volatile 只能让循环读到新值,不能中断 socketRead 或 sleep
是否需要跨 JVM 生效volatile 只在同一个 JVM 内有效,跨服务要用配置中心、Redis、MQ

常见坑 ​

坑后果正确做法
认为 volatile 等于线程安全复合操作仍然丢更新简单原子更新用 Atomic,复杂逻辑用锁
多字段分别 Atomic字段之间不一致封装不可变对象整体 CAS,或加锁
CAS 无限重试CPU 飙高限制重试、退避、分片或改锁
LongAdder 做严格判断sum() 不是强一致快照严格条件更新用 Atomic 或锁
volatile 控制跨服务开关只对当前 JVM 有效用配置中心或分布式存储
AtomicReference 更新可变对象引用没变但对象内部被改乱用不可变对象整体替换

面试标准回答 ​

volatile 有什么作用 ​

volatile 主要保证可见性和有序性。对 volatile 变量的写 happens-before 后续对这个变量的读,所以一个线程写 volatile 前的普通写,对另一个读到该 volatile 值的线程可见。同时 volatile 会通过内存屏障限制关键重排序。但它不保证复合操作原子性,所以 volatile int count 的 count++ 仍然不安全。

volatile 为什么不能保证 count++ ​

因为 count++ 不是单次写,而是读取、加一、写回三步。多个线程可能同时读到相同旧值,再各自写回相同新值,导致丢失更新。volatile 只能保证每次读写的可见性,不能把三步合成不可打断的整体。

AtomicInteger 为什么线程安全 ​

AtomicInteger 内部使用 volatile 保存值,用 CAS 保证更新原子性。自增时先读取旧值,计算新值,再用 CAS 判断内存当前值是否仍然等于旧值;如果是就写入新值,如果不是说明被其他线程改过,就重新读取并重试。

CAS 有什么问题 ​

CAS 常见问题有三个:ABA、自旋开销和只能原子更新单个变量。ABA 可以通过版本号或 AtomicStampedReference 解决;自旋开销在高竞争下会导致 CPU 升高;多个字段一致性不能靠多个 CAS 随便拼,应该用锁或不可变对象整体替换。

LongAdder 和 AtomicLong 怎么选 ​

AtomicLong 适合需要精确当前值和条件 CAS 更新的场景;LongAdder 适合高并发统计指标,它通过多个 Cell 分散竞争,吞吐通常更高,但 sum() 不是严格一致快照,所以不适合做强一致条件判断。

关联知识点 ​

知识点为什么要看
volatile与Atomic基础快速回顾基本概念和入门 Demo
Java内存模型理解 happens-before、可见性和有序性
CAS原理深入理解比较并交换、ABA 和自旋
synchronized全过程对比 volatile、Atomic 和锁的边界
AQS与JUC全过程理解 JUC 里 volatile state + CAS 的通用模型