如何解决并发问题,首先要理解并发问题的实际源头怎么发生的。编程
现代计算机的不一样硬件的运行速度是差别很大的,这个你们应该都是知道的。缓存
计算机数据传输运行速度上的快慢比较:
CPU > 缓存 > I/O
如何最大化的让不一样速度的硬件能够更好的协调执行,须要作一些“撮合”的工做并发
上面说来并发才生问题的背景,下面说下并发产生的具体缘由是什么编程语言
先看下单核CPU和缓存之间的关系:性能
单核状况下,也是最简单的状况,线程A操做写入变量A,这个变量A的值确定是被线程B所见的。由于2个线程是在一个CPU上操做,所用的也是同一个CPU缓存。优化
这里咱们来定义spa
一个线程对共享变量的修改,另一个线程可以马上看到,咱们称为 “可见性”
多核CPU时代下,咱们在来看下具体状况:操作系统
很明显,多核状况下每一个CPU都有本身的高速缓存,因此变量A的在每一个CPU中多是不一样步的,不一致的。
结果程A恰好操做来CPU1的缓存,而线程B也恰好只操做了CPU2的缓存。因此这状况下,当线程A操做变量A的时候,变量并不对线程B可见。线程
咱们用一段经典的代码说明下可见性的问题:code
private void add10K() { int idx = 0; while (idx++ < 100000) { count += 1; } } @Test public void demo() { // 建立两个线程,执行 add() 操做 Thread th1 = new Thread(() -> { add10K(); }); Thread th2 = new Thread(() -> { add10K(); }); // 启动两个线程 th1.start(); th2.start(); // 等待两个线程执行结束 try { th1.join(); th2.join(); } catch (Exception exc) { exc.printStackTrace(); } System.out.println(count); }
你们应该都知道,答案确定不是 200000
这就是可见性致使的问题,由于2个线程读取变量count
时,读取的都是本身CPU下的高速缓存内的缓存值,+1
时也是在本身的高速缓存中。
进程切换最先是为了提升CPU的使用率而出现的。
好比,50毫米操做系统会从新选择一个进程来执行(任务切换),50毫米成为“时间片”
早期的操做系统是进程间的切换,进程间的内存空间是不共享的,切换须要切换内存映射地址,切换成本大。
而一个进程建立的全部线程,内存空间都是共享的。因此如今的操做系统都是基于更轻量的线程实现切换的,如今咱们提到的“任务切换”都是线程切换。
任务切换的时机大多数在“时间片”结束的时候。
如今咱们使用的基本都是高级语言,高级语言的一句对应多条CPU命令,好比 count +=1
至少对应3条CPU命令,指令:
1, 从内存加载到CPU的寄存器
2, 在寄存器执行 +1
3, 最后,讲结果写回内存(缓存机制致使可能写入的是CPU缓存而不是内存)
操做系统作任务切换,会在 任意一条CPU指令执行完就行切换。因此会致使问题
如图所示,线程A当执行完初始化count=0
时候,恰好被线程切换给了线程B。线程B执行count+1=1
并最后写入值到缓存中,CPU切换回线程A后,继续执行A线程的count+1=1
并再次写入缓存,最后缓存中的count仍是为1.
一开始咱们任务count+1=1应该是一个不能再被拆开的原子操做。
咱们把一个或多个操做在CPU执行过程当中的不被中断的特性称为 原子性。
CPU可以保证的原子性,是CPU指令级别的。因此高级语言须要语言层面 保证操做的原子性。
有序性
。顾名思义,有序性指的是程序按照代码的前后顺序执行。
编译器为了优化性能,有时候会改变程序中语句的前后顺序,例如程序中:a=6;b=7;
编译器优化后可能变成b=7;a=6;
,在这个例子中,编译器调整了语句的顺序,可是不影响程序的最终结果。不过有时候编译器及解释器的优化可能致使意想不到的 Bug。
Java中的经典案例,双重检查建立单例对象;
public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
看似完美的代码,其实有问题。问题就在new
上。
想象中 new操做步骤:
1,分配一块内存 M
2,在内存M上 初始化对象
3,把内存M地址赋值给 变量
实际上就行编译后的顺序是:
1,分开一块内存 M
2,把内存M地址赋值给 变量
3,在 内存M上 初始化对象
优化致使的问题:
如图所示,当线程A执行到第二步的时候,被线程切换了,这时候,instance未初始化实例的对象,而线程B这时候执行到instance == null ?
的判断中,发现instance已经有“值”了,致使了返回了一个空对象的异常。
1,缓存引起的可见性问题
2,切换线程带来的原子性问题
3,编译带来的有序性问题
深入理解这些来龙去脉,能够诊断大部分并发的问题!