CPU优化导致并发异常的三个问题
-
CPU增加缓存,均衡与内存间的速度差异(可见性问题):
-
可见性:一个线程对共享变量的修改,能立刻被其他线程嗅探到。
-
对于多核设备,每个线程被分配在一个处理器上运行,都有各自的CPU缓存。
-
又因为同一个进程上的多个线程共享同一块内存空间。
-
单核理想情况下:线程运算的结果首先进入各自CPU缓存后存入内存,其他线程的CPU缓存嗅探到内存中变量值改变,使自身缓存无效。
-
但是在并发情况下,多线程同时对共享变量值修改,无法即使刷新入内存,导致实际上运算利用自身CPU缓存值,这就导致可见性问题。
-
-
操作系统增加进程和线程,分时复用均衡与I/O间差异(原子性问题):
- 进程和线程利用基于时间片控制的多任务切换来提高CPU利用率。
- 然而高级语言的原子性和底层CPU指令的原子性具有差异:一条高级语言可能由多条CPU指令来执行,如果在完成其中某条CPU指令后进行了线程切换(即到达时间片规定时间),导致语义错误,
- 例如:在线程A,B中分别对共享变量cnt=0执行cnt+=1操作
-
CPU指令1:将内存中cnt值写入CPU寄存器
-
CPU指令2:在CPU寄存器中进行值+1操作
-
CPU指令3:将CPU寄存器中更新的值写入内存
如图中,线程A中执行指令1,获得cnt=0,然而此时进行了任务切换,B中获得cnt=0,cnt+1,cnt=1存入内存之后再次任务切换,A执行,cnt+1,cnt=1存入内存,语句执行的原子性被打破导致了最终内存中cnt=1,而不是期待的cnt=2。
-
-
编译程序优化指令执行次序,高效利用缓存(有序性问题):
-
例:双重检查建立单例对象
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同时调用Singleton类中的getInstance方法时,都会进入第一个if判断发现instance对象为null
- 此时两个线程进入synchronized修饰的代码块,并且分别将对Singleton的Class对象进行加锁操作,由于互斥性,只有其中一个会加锁成功,获得这个临界区的使用权限。
- 于是其中一个线程创建新的单例对象instance,并且释放锁,阻塞队列中的第二个线程此时获得该代码块的锁,进入临界区发现已经存在一个instance对象,则不再创建新对象,正常返回。
- 指令执行顺序:分配地址块,在地址块上创建新的Singleton对象,将该地址指针指向instance
-
实际上:
- 指令优化后的执行顺序:分配地址块,将地址块指针指向instance,再给该地址块上创建
Singleton对象。
- 指令重排,导致指令2在指令3之后指向,若第一个线程执行完指令2 后发生任务切换,此时第二个线程将会看到 instance!=null 得到一个初始化为完成的Singleton对象,导致空指针异常。
-