面试官:你知道并发Bug的源头是什么吗?
讲实话听到这个问题,不太熟悉并发编程的同学有点晕,你可能只能答个因为多线程之间的竞争共享资源啊。对说的没错。
但是呢感觉不够亮眼!我们的目的就是让面试官眼前一亮,让他颤抖!心里鼓掌:"牛批牛批!"
万剑归宗多线程,给我们的感觉像啥?就像《风云》里面的无名的"万剑归宗"!《雪中悍刀行》里面老剑神李淳罡一声“剑来!”。哗哗哗的好多剑同时操作着一起飞!多帅哦!
但实际上CPU的执行是有时间片的,也就是其实我们多线程不是同一时间一起执行的,是每个线程执行一会儿就换一个上,但是时间片很短,所以我们感觉上是一起执行的。当然多核CPU的话,每个CPU是并行的,但是我们CPU颗数哪有我们的线程多!
大家都知道我们电脑有CPU,内存,IO设备。IO读取时最慢的,然后是内存读取,最快的是CPU的执行。那因为内存太慢了,跟不上CPU,所以CPU搞了个缓存。就是因为这个缓存再加上多颗CPU并发就出问题了。
举个例子
上面这个程序很简单,但是如果有两个线程A和B,分别CPU-A和CPU-B中同时addOne()可能会出现什么问题呢?
同时的线程A把a从内存中取到CPU-A缓存中,缓存中a的值是0,线程B把a从内存中取到CPU-B缓存中缓存中a的值是0。然后它们各自+了个1。这时候在它们的各自的缓存中a的值都是1。那之后写入内存中,是不是a就变成1了?
那我们加了两次是不是少了一次了!这是问题就是可见性问题了!CPU-A的缓存的值CPU-B不可见!这就是可见性的根本原因。
我们再来说一下原子性,首先原子性指的就是一个或多个操作在CPU中执行不会被中断的特性称为原子性。
我们的JAVA语言是高级语言。高级语言是怎么个情况。就是我们一条java代码涵盖了好几条底层指令。比如我们上面的a=a+1;把它转换成CPU指令至少有三条。
第一条:把a从内存拿到寄存器中;
第二条:寄存器中+1;
第三条:结果写入缓存或内存中;
那按照CPU的原子性,它是指令级别的!一条指令是不会被中断的,所以说在我们以为a=a+1;是原子级别的,实际上在CPU看来不是。
所以呢如果此时有两个线程同时执行,那可能线程A执行到第二条的时候,时间片到了。现在换线程B上了,然后线程B又把a从内存中拿到,然后+1返回结果,此时换到了线程A,线程A继续执行第三条。那情况就是又错了。
还没完呢!我们编译器或解释器的优化可能带来意料不到的问题!为了优化性能,它可能会改变语句执行的顺序,也就是指令重排!
最经典的就是单例的双重检查了!
看起来都加锁了好像没问题了,而问题就出在 instance = new Singleton(); 上
我们认为的new操作就是:
1.分配一块内存
2.在内存上初始化Singleton
3.将内存地址赋值给instance
而指令重排之后:
1.分配一块内存
2.将内存地址分配给instance
3.初始化Singleton
这会导致什么问题呢?假设线程A已经执行到new Singleton()的指令2了,然后时间片到了,这时候线程B也调用getInstance();到第一个instance==null时候,直接返回了。很开心拿到了对象了!而实际上对象还没初始化呢!所以用了的话就是空指针了!
这就是有序性问题了!
所以一共有三大特性:可见性、原子性、有序性。并发的问题都逃不过这三大特性!JAVA的内存模型其实有很多都是解决这几种问题的,比如上面单例的例子instance由 volatile来声明一下,这样就禁止指令重排了!
希望通过今天的分析大家能深入理解这三大特性!写出健壮的并发代码!
如果有错误欢迎指正!以下是我的个人公众号!请大家多多支持谢谢!