三类并发问题
-
多核CPU缓存带来的可见性问题
线程 A 和线程 B 同时开始执行,那么第一次都会将 count=0 读到各自的 CPU 缓存里,执行完 count+=1 之后,各自 CPU 缓存里的值都是 1,同时写入内存后,会发现内存中是 1,而不是期望的 2
ec6743e74ccf9a3c6d6c819a41e52279.png -
CPU 线程切换导致的原子性问题
count +=1;对应CPU的三条指令
指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
指令 2:之后,在寄存器中执行 +1 操作;
指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)
如果执行序列为如下情况就会出现问题,可见count+=1 并不是原子操作,只有CPU级别的指令才是原子的
33777c468872cb9a99b3cdc1ff597063.png - 编译优化带来的有序性问题
通过双重检查单例模式的例子说明:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
问题出现在 new Singleton()的时候,原以为的CPU指令执行顺序:
1.分配一块内存 M;
2.在内存 M 上初始化 Singleton 对象;
3.然后 M 的地址赋值给 instance 变量。
实际上经过优化后的指令执行顺序:
1.分配一块内存 M;
2.把M 的地址赋值给 instance 变量;
3.在内存 M 上初始化 Singleton 对象。
有可能出现的问题:
A线程执行完步骤2后,B线程在第一个判空语句时,instance不再是null,直接返回instance实例,但是该实例并没有经过初始化,引用成员变量时会出现空指针异常。
第一个和第三个问题可以使用volatile关键字解决,volatile可以实现资源的可见性并且可以避免指令重排序。第二个问题可以加锁。