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

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

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

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

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

相关推荐
admiraldeworm2 小时前
Spring Boot + Spring AI 最小可运行 Demo
java·人工智能·ai
chenglin0162 小时前
ES_数据存储知识
java·服务器·elasticsearch
fs哆哆3 小时前
在VB.net中一维数组,与VBA有什么区别
java·开发语言·数据结构·算法·.net
蝎子莱莱爱打怪3 小时前
Hadoop3.3.5、Hbase2.6.1 集群搭建&Phoenix使用记录
大数据·后端·hbase
johnZhangqi3 小时前
深圳大学-计算机信息管理课程实验 C++ 自考模拟题
java·开发语言·c++
Sally璐璐3 小时前
Go语言变量声明与初始化详解
java·开发语言·golang
你的人类朋友4 小时前
git常见操作整理(持续更新)
前端·git·后端
你的人类朋友4 小时前
git中的Fast-Forward是什么?
前端·git·后端
C4程序员4 小时前
北京JAVA基础面试30天打卡14
java·开发语言·面试