并发编程三大特性全解析:原子性、可见性、有序性,一文讲透!

在并发编程中,很多问题的根源都离不开三个关键词:原子性、可见性、有序性

这三大特性共同构成了并发编程的核心,也是 Java 内存模型(Java Memory Model, JMM)的基础。

理解这三大特性,不仅能帮助我们写出正确的多线程程序,也是面试的高频考点。本文将从定义、问题场景、代码示例和解决方案逐一解析。


一、原子性(Atomicity)

1. 什么是原子性?

原子性意味着一个操作 不可分割,要么全部执行成功,要么完全不执行,不会被线程调度器中断。

例如:

java 复制代码
int count = 0;
count++; 

这看似一个简单的自增操作,实际上包含三步:

  1. 读取 count 的值
  2. 值加 1
  3. 写回内存

在多线程环境下,这三步可能被打断,导致多个线程同时读到相同值,覆盖更新,最终结果小于预期。


2. 原子性问题的典型场景

java 复制代码
public class AtomicityDemo {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) count++;
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) count++;
        });

        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println(count);
    }
}

理论结果应为 20000,但实际结果往往小得多,就是因为 count++ 不是原子操作。


3. 如何保证原子性?

  • synchronized:保证同一时刻只有一个线程执行临界区代码。
  • Lock(如 ReentrantLock) :更灵活的锁机制。
  • 原子类(AtomicInteger 等) :基于 CAS(Compare-And-Swap)实现的无锁原子操作。

示例:AtomicInteger

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) count.incrementAndGet();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) count.incrementAndGet();
        });

        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println(count); // 始终为20000
    }
}

二、可见性(Visibility)

1. 什么是可见性?

在多线程中,每个线程会将共享变量缓存到 工作内存(CPU 缓存)

如果一个线程修改了变量,其他线程未必立刻能看到最新值。

2. 可见性问题的典型场景

java 复制代码
public class VisibilityDemo {
    static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {
                // 空循环
            }
            System.out.println("线程结束");
        }).start();

        Thread.sleep(1000);
        flag = false; // 修改flag
    }
}

理想情况下,子线程应在 1 秒后结束,但有时会一直死循环。原因是子线程一直从自己的缓存读 flag,看不到主线程更新的值。


3. 如何保证可见性?

  • volatile 关键字:保证变量修改对其他线程立即可见。
  • synchronized / Lock:加锁的过程会刷新工作内存,保证可见性。

示例:volatile

java 复制代码
public class VolatileDemo {
    static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {}
            System.out.println("线程结束");
        }).start();

        Thread.sleep(1000);
        flag = false;
    }
}

此时线程能如期结束,因为 volatile 保证了可见性。


三、有序性(Ordering)

1. 什么是有序性?

为了提升性能,编译器和 CPU 可能会对指令进行重排序(Reordering)。

在单线程环境下,重排序不会改变执行结果,但在多线程环境下可能导致问题。

2. 有序性问题的典型场景 ------ 双重检查锁(DCL)单例

java 复制代码
public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

问题在于 instance = new Singleton(); 不是原子操作,实际包含:

  1. 分配内存
  2. 初始化对象
  3. 将对象引用赋给 instance

CPU 可能会发生重排序:先执行 3,再执行 2。

导致其他线程拿到一个"未初始化完成"的对象。


3. 如何保证有序性?

  • volatile:禁止指令重排,保证初始化顺序正确。
  • synchronized / Lock:天然保证有序性。

正确的 DCL 单例写法:

java 复制代码
public class Singleton {
    private static volatile Singleton instance; // 加volatile

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

四、三大特性与 Java 内存模型(JMM)的关系

JMM 的目标就是通过 happens-before 原则 来定义多线程间的可见性和有序性。

  • 原子性:由锁和原子类保证。
  • 可见性:由 volatile、synchronized、final 保证。
  • 有序性:由 volatile、synchronized、happens-before 规则保证。

常见的 happens-before 规则:

  1. 程序顺序规则:单线程中,前面的操作先发生于后续操作。
  2. 锁规则:解锁先于加锁。
  3. volatile 规则:对 volatile 变量的写操作先于读操作。
  4. 线程启动规则:Thread.start() 先于线程 run() 中操作。
  5. 线程终止规则:线程中的所有操作先于 join() 返回。

五、三大特性在面试中的高频考点

  1. 原子性考点

    • i++ 为什么不是线程安全的?如何解决?
    • CAS 与 synchronized 的区别?
  2. 可见性考点

    • volatile 能保证原子性吗?为什么?
    • 为什么需要工作内存与主内存模型?
  3. 有序性考点

    • DCL 单例为什么要加 volatile?
    • CPU/编译器指令重排会带来哪些问题?

六、总结

  • 原子性 :操作不可分割,典型问题是 i++。解决方案:锁、原子类。
  • 可见性 :线程之间修改对方不可见,典型问题是 volatile。解决方案:volatile、synchronized。
  • 有序性:编译器和 CPU 重排引发问题,典型问题是 DCL 单例。解决方案:volatile、锁、happens-before 规则。

并发编程的本质就是:围绕原子性、可见性、有序性进行权衡和优化

理解这三大特性,才能真正写出正确、高效的多线程程序。

相关推荐
葫芦和十三1 天前
图解 MongoDB 19|Oplog:复制的真正载体,不是文档是操作
后端·mongodb·agent
葫芦和十三1 天前
图解 MongoDB 20|复制延迟与 catch up:Secondary 为什么跟不上
后端·mongodb·agent
IT_陈寒1 天前
SpringBoot自动配置的坑,我的API突然就404了
前端·人工智能·后端
ServBay1 天前
为什么说 MCP 是 2026 年开发者必须掌握的黄金协议?
后端·mcp
程序员夏洛1 天前
Spring Boot 多模块项目中 IDEA 提示 Cannot resolve symbol 的一次排查记录
后端
子兮曰1 天前
OpenMontage 深度解剖:你的 AI 编程助手,其实是个视频工作室
前端·后端·ai编程
子兮曰1 天前
前端工具链的「Rust 化」:一场没有赢家的军备竞赛?
前端·后端·rust
爱勇宝1 天前
从 Ctrl+CV 到 Enter:程序员正在失去什么
前端·后端·程序员
码事漫谈1 天前
EdgeOne Makers + WorkBuddy:零基础也能快速搭建可上线的 AI 智能体(附图文教程)
后端