并发问题的根本
围绕着原子性、有序性、可见性,会出现各种的并发问题,要理解透彻这三种特性,才能有效的定位出现的并发问题(并发问题往往是综合性的)
可见性
例如:不同的CPU缓存造成的可见性问题
假设一个场景,调用add方法,count+=1执行1w次,在代码编译后,CPU指令为如下3步骤
1、将count读到cpu
2、进行+1操作
3、count写回内存中(有可能写入cpu缓存,但cpu缓存再写入内存时间不可控)
现在线程A、B同时执行add方法,执行结果理想为count=2w,但实际结果往往是0-2w之间。
A | B |
---|---|
COUNT读到内存(count=0) | COUNT读到内存( count=0 ) |
+1 | +1 |
count写回内存中 | count写回内存中 |
同时将count=0读到cpu,结果导致执行完,本省应该=2,但=1
由此可见,不同CPU由于对变量数据不可见,并且操作不是原子性,数据更新不及时,会导致出现并发问题
原子性
切换CPU导致的原子性问题
原子性,一个操作不是原子性,那么在并发环境下,往往是不可靠的
例如上面的问题
有序性
cpu执行指令前,会对待执行指令进行重排序优化,我们使用的都是高级语言进行编程,很多语句,在编译后并不按照理想的流程排序。
例如Java语言的二阶段判断,保障获取到单例实例,不会导致后边的程序执行空指针。
通常如下:
class Singleton{
static Singleton instance;
static Singleton getInstance(){
if(instance==null){
synchronized (instance){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
理想情况下,A、B两个线程,同时执行getInstance(),A发现instance未初始化,拿到锁,去初始化,B等待A初始化完成后释放锁,拿到锁资源,判断,已经初始化,直接返回结果。
但是,毕竟是理想情况下,这个情况,我们都是假定,初始化流程如下才可:
- 开辟内存空间
- 根据对象类型初始化
- 将内存地址指向变量instance
如果,代码在编译后,指令进行重排序,则会为
- 开辟内存空间
- 内存地址指向变量instance
- 根据数据类型初始化
哎嗨!这就坏菜了
A执行到2步骤,B进行第一个判断,发现已经指向内存地址,变量对象不为空,直接返回。
但是此时对象未被初始化,调用获取属性的方法,会抛出异常。