CPU缓存导致可见性问题
单核CPU中,每个线程在同一颗CPU上运行,一个线程对缓存的写操作,对另一个线程来说是可见的。
多核CPU中,每颗核心有独自的缓存,多个线程在不同的核心上运行,操作的是不同的缓存。如:
线程 A 操作的是 CPU-1
上的缓存,而线程 B 操作的是 CPU-2
上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。
线程切换导致的原子性问题
高级语言中的一条语句,往往由多条CPU指令完成,如 count += 1
包含三条指令:
- 从内存中加载变量
count
到CPU寄存器; - 对寄存器执行立即数加操作;
- 将寄存器的值,写入内存(缓存机制导致可能写入缓存而不是内存)。
CPU切换线程可能发生在任何一条指令执行完之后,将会导致程序执行结果异常,如:
编译优化导致有序性问题
编译器为了优化性能,可能会更改程序中代码的执行顺序。
例:双重检查创建单例对象
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
假设有两个线程 A、B 同时调用 getInstance()
方法,他们会同时发现 instance == null
,于是同时对 Singleton.class
加锁,
- 此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);
- 线程 A 会创建一个
Singleton
实例; - 之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的;
- 加锁成功后,线程 B 检查
instance == null
时会发现,已经创建过Singleton
实例了,所以线程 B 不会再创建一个Singleton
实例。
此 getInstance()
方法中的 new
操作所期待的执行路径应该是:
- 分配一块内存 M;
- 在内存 M 上初始化
Singleton
对象; - 然后 M 的地址赋值给
instance
变量。
经过指令重排后的执行路径是:
- 分配一块内存 M;
- 将 M 的地址赋值给
instance
变量; - 最后在内存 M 上初始化
Singleton
对象。
将会导致空指针异常:
- 我们假设线程 A 先执行
getInstance()
方法; - 当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;
- 如果此时线程 B 也执行
getInstance()
方法,那么线程 B 在执行第一个判断时会发现instance != null
,所以直接返回instance
; - 而此时的
instance
是没有初始化过的,如果我们这个时候访问instance
的成员变量就可能触发空指针异常。