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

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

这三大特性共同构成了并发编程的核心,也是 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 规则。

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

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

相关推荐
葫芦和十三2 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp2 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑3 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯4 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan6 小时前
多Agent之间的区别
后端
青石路7 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充8 小时前
1.面向对象设计思想
后端
IT_陈寒8 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro9 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗9 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端