Java并发编程【并发BUG的源头】
1. 缓存导致的可见性问题
在多核cpu时代,cpu缓存的同步会导致共享变量的操作结果在多个线程之间不可见,进而导致并发问题。
int count=0;
如图若线程A和线程B同时做 count+=1;操作,得到的结果可能并不是我们想要的 count=2 而是 count=1
如此若循环加 1万 次 count 的结果接近 2万 而不是 2万。若循环 1亿 次效果将更明显 count 的结果接近 1亿 而不是 2亿
多线程对变量 V 的操作过程如下图
2. 线程切换导致的原子性问题
我们使用的高级编程语言一条语句往往需要多条 cpu 指令来完成 如:
count+=1;对应 cpu 指令需要三条来完成
指令1:将变量 count 从内存加载到寄存器中
指令2:在寄存器执行 count+=1 操作
指令3:将值写入内存(缓存机制可能导致写入 cpu 缓存而不是内存)
这个时候线程A如果刚好执行到指令1后指令2执行之前发生了线程切换,此时由线程B切换进来执行 count+=1 操作,线程B执行完之后线程A继续执行,将导致结果 count=1 而不是我们所期望的 count=2。执行过程如下图。
3. 编译优化导致的有序性问题
有序性是指程序按照先后顺序有序执行,而编译器有时候为了优化性能会对代码的执行顺序进行优化,比如
x=1
y=2
优化后的顺序为
y=2
x=1
这个例子虽然改变了顺序但没有影响结果。而有时候这样的优化会导致意想不到的结果。
我们先看一下双重锁创建单例的例子:
public class Singleton{
private static Singleton singleton;
private Singleton(){ }
public static Singleton getSingleton(){
if (singleton==null){
synchronized (Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
看起来似乎没有什么问题,但如果进行编译优化之后,在多线程中将会出现意想不到的bug
实际问题出现在 new 操作上
正常的我们所认为的 new 操作应该是这样的:
- 分配一块内存 W
- 在内存W上初始化 Singleton 对象
- 将 W 的地址赋值给 singleton 变量
实际优化过后的 new 操作:
- 分配一块内存 W
- 将 W 的地址赋值给 singleton 变量
- 在内存 W 上初始化 Singleton 对象
这样问题就出来了,我们假设线程 A 执行 getSingleton() 方法,执行到 new 操作之前刚好发生线程切换(注意此时因为编译优化的原因 singleton 变量已经得到地址赋值,也就是说不为 null ,但并未初始化对象),由线程 B 执行 getSingleton() 方法,此时线程 B 判断 singleton==null 值为 false 故直接返回,这时如果访问变量 singleton 将会触发空指针异常。
总结
总的来说引发并发编程问题的源头主要分为三种,缓存导致的可见性问题(如多核cpu缓存不同步,可见性是操作结果对其他线程不可见),线程切换导致的原子性问题(高级语言一条语句可能包含多条 cpu 指令,一条 cpu 指令是保证原子性的,但高级语言无法保证),编译优化导致的有序性问题(编译器优化时可能会优化语句执行顺序,导致意想不到的 BUG,参考典型的双重锁创建单例)。
结语
最后希望阅读完本篇文章你可以有所收获,如果你对并发编程有自己的看法,或者你有更好的并发编程经典案例,欢迎在评论区给我留言,我会及时给与反馈,感谢阅读。
还没有评论,来说两句吧...