1.CPU缓存导致的可见性问题
CPU缓存IO>内存IO>硬盘IO
单CPU情况下,所有线程都在同一个CPU喜爱执行,A线程对缓存的操作,对B线程来讲,一定是可见的,如下图


codeDemo
public class VisibilityDemo {
private static int count;
public static void addCount() {
int idx = 0;
while (idx++ < 10000) {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
addCount();
}, "t1");
Thread t2 = new Thread(() -> {
addCount();
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
上面的代码,直觉告诉我们count的结果应该是20000,但实际结果缺出乎我们的意料,他最终的结果会是一个10000 到 20000 之间的随机数,为什么呢? 我们假设A线程和B线程同时执行,A线程会将count的值读取到CPU-1的缓存中,B线程会将count的值读取到CPU-2的缓存中,执行完count+=1之后,各自缓存中的值都是1,然后写回到内存,内存中的值变成了1,而不是我们期望的2,之后的执行都会依据各自缓存中的值去计算,最终导致结果不是我们期望的结果。
2.线程切换导致的原子性问题
Java并发编程是基于多线程的,自然会涉及到任务切换,即线程的切换。 高级语言里一条语句往往需要多条 CPU 指令完成,例 如上面代码中的count += 1,至少需要三条 CPU 指令。

- 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
- 指令 2:之后,在寄存器中执行 +1 操作;
- 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
如果线程 A 在指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的序列执行,那么我们会发现 两个线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。

CPU保证的原子性是CPU指令级别的,不是高级语言的操作符,所以要保证高级语言编程操作的原子性
3.性能优化导致的指令重排
反例:创建一个实例
我们以为的new操作 1、分配一个内存M 2、在内存M上初始化singleton 3、将M地址赋值给instance变量
实际上有可能是这样的 1、分配一个内存地址M 2、将内存地址M赋值给instance变量 3、在内存地址M上初始化
单例模式的双重锁校验机制,如果允许指令重排,则会发生下图的异常情况,返回一个未初始化的instance
