Skip to content

volatile 与 Atomic

volatile 和 Atomic 原子类是 Java 并发的核心基础。它们经常出现在开关标记、状态刷新、计数器、无锁更新、并发限流等场景里。只会背“volatile 保证可见性,不保证原子性”是不够的,要理解为什么。

如果要系统理解 JMM 可见性、volatile 写读全过程、内存屏障、CAS 自旋、ABA、AtomicReference、LongAdder、商业场景和线上排查,建议继续看:volatile与Atomic全过程原理

并发里的三个问题

mermaid
flowchart TD
    A["并发问题"] --> B["可见性<br/>一个线程改了值<br/>另一个线程<br/>看不见"]
    B --> C["原子性<br/>多个操作被线程<br/>切换打断"]
    C --> D["有序性<br/>编译器或 CPU 重排序<br/>导致执行顺序变化"]
问题示例
可见性线程 A 把 running 改成 false,线程 B 仍然读到 true
原子性多线程同时执行 count++,结果少加
有序性对象还没初始化完,引用先被其他线程看到

volatile 解决什么

volatile 主要解决可见性和一定程度的有序性问题。

java
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 常用于发布状态。

java
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 可见性原理

mermaid
flowchart TD
    A["线程 A 写 volatile 变量"] --> B["写入主内存"]
    B --> C["通知其他线程缓存失效"]
    D["线程 B 读 volatile 变量"] --> E["从主内存读取最新值"]

可以简单理解为:volatile 要求变量的写入对其他线程尽快可见,读取时不能一直拿旧缓存。

更准确地说,volatile 会在编译器和 CPU 层面加入内存屏障语义,限制特定读写重排序,并让写入及时对其他线程可见。你不需要背具体屏障名字,但要理解它不是“让变量变成原子变量”,而是建立可见性和顺序约束。

volatile 为什么不能保证 count++

count++ 看起来是一行,实际包含三步:

mermaid
flowchart TD
    A["读取 count"] --> B["count 加 1"]
    B --> C["写回 count"]

多个线程同时执行时可能交叉:

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 只能保证每次读写尽量看见最新值,但不能把“读、加、写”三步合成不可打断的整体。

volatile 不能替代锁

如果一个业务动作需要同时修改多个变量,volatile 就不够。

java
class Account {
    private volatile int balance;
    private volatile int version;

    public void add(int money) {
        balance += money;
        version++;
    }
}

balance += moneyversion++ 都不是原子操作,而且两个变量之间也没有整体一致性。多个线程同时执行时,可能出现金额和版本不匹配。此时应该使用锁、事务或 CAS 状态对象整体替换。

AtomicInteger

Atomic 原子类基于 CAS 实现无锁更新。

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

CAS 更新流程

mermaid
flowchart TD
    A["读取旧值 old"] --> B["计算新值 new"]
    B --> C["比较内存当前值是否仍是 old"]
    C --> D["是:写入 new"]
    C --> E["否:说明被别人改过"]
    E --> A

CAS 的思想是:我更新前先确认别人没改过;如果被别人改了,就重新读取再试。

AtomicReference

不仅数字可以原子更新,对象引用也可以。

java
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 更适合,因为它把竞争分散到多个槽位。

mermaid
flowchart TD
    A["多个线程递增"] --> B["分散到多个 Cell"]
    B --> C["读取时汇总 base 和 Cells"]

Demo:

java
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
简单计数和状态 CASAtomicIntegerAtomicReference
高并发统计LongAdder
多个变量需要一起修改synchronizedLock
修改过程包含复杂业务逻辑锁或事务控制

商业场景 Demo:开关加计数器

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

这里 enabledvolatile 保证开关变化可见,requestCountLongAdder 支持高并发统计。

常见风险

问题后果建议
认为 volatile 能保证所有线程安全count++ 丢失更新原子更新用 Atomic 或锁
CAS 自旋过多CPU 占用升高高竞争下考虑锁或 LongAdder
多字段分别 Atomic状态不一致多字段一致性用锁
忽略 ABA 问题CAS 误判没有变化使用版本号或 AtomicStampedReference

本章小结

volatile 适合解决可见性和禁止关键重排序,不适合复合操作原子性。Atomic 类通过 CAS 实现无锁原子更新,适合简单状态和计数。并发设计不要只看“能不能无锁”,要看竞争强度、状态一致性和业务复杂度。