计算机的 CPU、内存、I/O 设备的速度一直存在较大的差别,依次是 CPU > 内存 > I/O 设备,为了权衡这三者的速度差别,主要提出了三种解决办法:java
三种解决办法虽然有效,可是也带来了另外的三个问题,分别就是并发 bug 产生的源头。缓存
1.可见性问题并发
若是是单核 CPU,多个线程操做的都是同一个 CPU 缓存,那么一个线程修改了共享变量,另外一个线程确定能立刻看到。优化
若是是多核 CPU ,每一个 CPU 都有本身的缓存,这样线程对共享变量的修改便对其余线程不可见了。spa
2.原子性问题线程
为何会有线程切换?一个线程在执行的过程当中,可能会进行耗时的 I/O 操做,这时线程须要等待 I/O 操做完成。线程在等待的过程当中,能够释放 CPU 的使用权,让另外一个线程执行,这样可以提升 CPU 的使用率。code
例如上图,两个线程同时对变量 count 加 1,线程 A 在执行的过程当中切换到了线程 B,最后致使写入到内存的值都是 1,与预期不符。对象
3.有序性问题blog
首先看一段很经典的获取单例对象的代码:进程
public class Singleton { private static Singleton instance; //Java 获取单例对象 public Singleton getInstance(){ if (instance == null){ synchronized (Singleton.class){ if (instance == null){ instance = new Singleton(); } } } return instance; } }
程序的逻辑是:首先判断 instance 是否为空,若是为空,对其加锁,而后再判断是否为空,此时为空的话则初始化 instance 对象。
若是线程 A 和 B 同时执行方法,在 synchronized 处,一个线程会被阻塞,假设被阻塞的是线程 B,此时线程 A 进入并初始化 instance,而后唤醒线程 B,线程 B 进入的时候,发现 instance 不为空了,因此不会建立对象。
可是由于有序性问题的存在,这段代码也不是想象的那么完美,咱们指望的初始化对象的过程是这样的:1.分配内存;2.初始化对象;3.将内存地址赋给 instance。可是通过编译优化以后,倒是这样的:
这样,若是线程 A 执行到了第二步,而后切换到 线程 B,线程 B 就会认为 instance 不为空而后直接返回了,实际上 instance 并无初始化。
最后,总结一下,致使并发问题的三个源头分别是