Java并发编程【并发BUG的源头】

ゝ一纸荒年。 2022-10-02 13:44 248阅读 0赞

1. 缓存导致的可见性问题

在多核cpu时代,cpu缓存的同步会导致共享变量的操作结果在多个线程之间不可见,进而导致并发问题。

  1. int count=0
  2. 如图若线程A和线程B同时做 count+=1;操作,得到的结果可能并不是我们想要的 count=2 而是 count=1
  3. 如此若循环加 1 count 的结果接近 2 而不是 2万。若循环 1亿 次效果将更明显 count 的结果接近 1亿 而不是 2亿

多线程对变量 V 的操作过程如下图

在这里插入图片描述

2. 线程切换导致的原子性问题

我们使用的高级编程语言一条语句往往需要多条 cpu 指令来完成 如:

  1. count+=1;对应 cpu 指令需要三条来完成
  2. 指令1:将变量 count 从内存加载到寄存器中
  3. 指令2:在寄存器执行 count+=1 操作
  4. 指令3:将值写入内存(缓存机制可能导致写入 cpu 缓存而不是内存)

这个时候线程A如果刚好执行到指令1后指令2执行之前发生了线程切换,此时由线程B切换进来执行 count+=1 操作,线程B执行完之后线程A继续执行,将导致结果 count=1 而不是我们所期望的 count=2。执行过程如下图。
在这里插入图片描述

3. 编译优化导致的有序性问题

有序性是指程序按照先后顺序有序执行,而编译器有时候为了优化性能会对代码的执行顺序进行优化,比如

  1. x=1
  2. y=2
  3. 优化后的顺序为
  4. y=2
  5. x=1

这个例子虽然改变了顺序但没有影响结果。而有时候这样的优化会导致意想不到的结果。
我们先看一下双重锁创建单例的例子:

  1. public class Singleton{
  2. private static Singleton singleton;
  3. private Singleton(){ }
  4. public static Singleton getSingleton(){
  5. if (singleton==null){
  6. synchronized (Singleton.class){
  7. if(singleton==null){
  8. singleton=new Singleton();
  9. }
  10. }
  11. }
  12. return singleton;
  13. }
  14. }

看起来似乎没有什么问题,但如果进行编译优化之后,在多线程中将会出现意想不到的bug
实际问题出现在 new 操作上

正常的我们所认为的 new 操作应该是这样的:

  1. 分配一块内存 W
  2. 在内存W上初始化 Singleton 对象
  3. 将 W 的地址赋值给 singleton 变量

实际优化过后的 new 操作:

  1. 分配一块内存 W
  2. 将 W 的地址赋值给 singleton 变量
  3. 在内存 W 上初始化 Singleton 对象

这样问题就出来了,我们假设线程 A 执行 getSingleton() 方法,执行到 new 操作之前刚好发生线程切换(注意此时因为编译优化的原因 singleton 变量已经得到地址赋值,也就是说不为 null ,但并未初始化对象),由线程 B 执行 getSingleton() 方法,此时线程 B 判断 singleton==null 值为 false 故直接返回,这时如果访问变量 singleton 将会触发空指针异常。在这里插入图片描述

总结

总的来说引发并发编程问题的源头主要分为三种,缓存导致的可见性问题如多核cpu缓存不同步,可见性是操作结果对其他线程不可见),线程切换导致的原子性问题高级语言一条语句可能包含多条 cpu 指令,一条 cpu 指令是保证原子性的,但高级语言无法保证),编译优化导致的有序性问题编译器优化时可能会优化语句执行顺序,导致意想不到的 BUG,参考典型的双重锁创建单例)。

结语
最后希望阅读完本篇文章你可以有所收获,如果你对并发编程有自己的看法,或者你有更好的并发编程经典案例,欢迎在评论区给我留言,我会及时给与反馈,感谢阅读。

发表评论

表情:
评论列表 (有 0 条评论,248人围观)

还没有评论,来说两句吧...

相关阅读

    相关 并发编程Bug源头

    本文来自《Java并发编程实战》的第一篇《可见性、原子性和有序性问题:并发编程Bug的源头》,主要介绍了并发问题出现的原因。更多文章查看:[Java并发学习记录总目录][Jav