一、回答重点
原子性、可见性、有序性是 Java 并发编程的三大核心特性,任何并发 bug 基本都能归到这三类里面。
1. 原子性
原子性指一个操作要么全部执行完,要么压根没执行,中间不会被其他线程打断。比如 i++ 这个操作看着像一行代码,实际上是读取、加 1、写回三个步骤,多线程环境下就可能出问题。
2. 可见性
可见性指一个线程修改了共享变量,其他线程能立刻看到最新值。CPU 有自己的高速缓存,线程修改的值可能还躺在缓存里没刷回主内存,别的线程就读到了旧值。
3. 有序性
有序性指程序执行顺序和代码写的顺序一致。编译器和 CPU 为了性能会对指令做重排序,单线程下没问题,多线程就可能出现诡异的 bug。
下面用一个经典例子演示这三个问题是如何导致 Bug 的:
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题就出在这
}
}
}
return instance;
}
}
这段双重检查锁定看起来没毛病,但 instance = new Singleton() 这行代码实际上分三步:分配内存空间、初始化对象、把引用指向内存地址。CPU 可能把第 2 步和第 3 步重排序,导致另一个线程拿到一个还没初始化完的对象,直接空指针。解决办法就是给 instance 加上 volatile。
二、扩展知识
1. 原子性的保障手段
Java 里保证原子性主要靠两种方式:锁和 CAS。
synchronized 和 Lock 是最直接的手段,进入临界区的线程独占资源,其他线程只能干等着。但锁的开销不小,线程切换、阻塞唤醒都是重量级操作。
CAS 是一种乐观锁思路,底层依赖 CPU 的 cmpxchg 指令。比如 AtomicInteger 的 incrementAndGet,它会不断尝试'比较当前值是否等于预期值,等于就更新',失败就重试。CAS 避免了线程阻塞,但在竞争激烈时会疯狂自旋,CPU 空转。
// AtomicInteger 的自增底层就是 CAS
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();


