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 操作应该是这样的:

  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,参考典型的双重锁创建单例)。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dying 搁浅

两杯酒,一杯敬你余生多欢喜。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值