在并发编程中,很多问题的根源都离不开三个关键词:原子性、可见性、有序性 。
这三大特性共同构成了并发编程的核心,也是 Java 内存模型(Java Memory Model, JMM)的基础。
理解这三大特性,不仅能帮助我们写出正确的多线程程序,也是面试的高频考点。本文将从定义、问题场景、代码示例和解决方案逐一解析。
一、原子性(Atomicity)
1. 什么是原子性?
原子性意味着一个操作 不可分割,要么全部执行成功,要么完全不执行,不会被线程调度器中断。
例如:
java
int count = 0;
count++;
这看似一个简单的自增操作,实际上包含三步:
- 读取 count 的值
- 值加 1
- 写回内存
在多线程环境下,这三步可能被打断,导致多个线程同时读到相同值,覆盖更新,最终结果小于预期。
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();
不是原子操作,实际包含:
- 分配内存
- 初始化对象
- 将对象引用赋给 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 规则:
- 程序顺序规则:单线程中,前面的操作先发生于后续操作。
- 锁规则:解锁先于加锁。
- volatile 规则:对 volatile 变量的写操作先于读操作。
- 线程启动规则:
Thread.start()
先于线程 run() 中操作。 - 线程终止规则:线程中的所有操作先于
join()
返回。
五、三大特性在面试中的高频考点
-
原子性考点
i++
为什么不是线程安全的?如何解决?- CAS 与 synchronized 的区别?
-
可见性考点
- volatile 能保证原子性吗?为什么?
- 为什么需要工作内存与主内存模型?
-
有序性考点
- DCL 单例为什么要加 volatile?
- CPU/编译器指令重排会带来哪些问题?
六、总结
- 原子性 :操作不可分割,典型问题是
i++
。解决方案:锁、原子类。 - 可见性 :线程之间修改对方不可见,典型问题是
volatile
。解决方案:volatile、synchronized。 - 有序性:编译器和 CPU 重排引发问题,典型问题是 DCL 单例。解决方案:volatile、锁、happens-before 规则。
并发编程的本质就是:围绕原子性、可见性、有序性进行权衡和优化 。
理解这三大特性,才能真正写出正确、高效的多线程程序。