在计算机的组成中,cpu、内存、I/O设备的关系如上图所示。虽然这些设备都在不断更新迭代,但是有个致命缺陷一直都存在,那就是这三者之间的速度差异,可以粗略的说:v(cpu)>>(cache)>>v(disk),尽管已从机械硬盘过渡到固态硬盘,内存性能还是远远大于固态硬盘的,这就意味着单方面提高cpu的性能是无效的,因为最终受制于硬盘的性能。
为了提高cpu的利用率,让cpu性能不受制于性能最差的I/O设备,计算机体系作出以下几点变化:
1.增加cpu缓存,cpu缓存的性能优于内存,将cpu的运算结果预先写入到缓存中,然后再写入到内存中,平衡cpu与内存之间的差异。
2.操作系统增加进程、线程,分时服用cpu,从而平衡cpu与I/O设备的性能差异。
3.指令重排序。
在并发编程的过程出现的诡异问题,也都是上面三个原因引起的。
一:缓存导致可见性问题
假设线程A,线程B都对变量a进行操作,不管哪个线程对a进行修改,对另外一个线程来说都是立马可见的,因为两个线程操作的是同一个缓存。
但是当我们步入多核cpu的时代后,情形就不一样了。
线程A,线程B要对变量a进行操作时,都先回从内存中读取a的值,放到各自的cpu缓存中,这就会造成线程A、B对变量a的修改不能及时反应到另外一个线程中,造成不符合预期的运算。
二:线程切换带来的原子性问题
在编程中我们写的c++、java都可以被称为高级语言,高级语言的一条语句的运行绝不是表面上看起来那么简单完成的,比如: a=a+1,就至少需要三条cpu指令才能完成。
1.加载a的值到CPU指令寄存器。
2.cpu对a进行加1
3.cpu将运算值写入到内存(或者cpu缓存)
操作系统对线程的调用、切换可以发生在任何一条cpu指令完成之后,这就会导致意料之外的结果。假设a=0,现在线程A和线程B都对a进行加1运算,那么结果是多少呢?
很显然,a最终的结果不是我们预期的2,这就是原子性带来的并发问题。
三:指令重排带来的有序性问题
1. a=5
2. b=6
3. ...
在一个线程中对于, a=5 和 b=6谁先运行对代码的运行结果并没有什么影响的时候,jvm会根据实际情况进行指令重排,也就是代码的运行顺序不一定是按照我们写的顺序。上面的代码在一个线程中运行的时候不会有任何问题,假设现在有另外一个线程:
线程A 线程B
1: a=5 1: if(b==6){
2:b=6 2: 2/a;
3: . ... 3: }
我们预期的是在b的值为6、a的值为5的时候,就对a的值进行操作,但是实际情况中会存在a不为5但是一样吧对a作为分母的操作的情况,如果a初始值为0,就会导致程序出现异常。
总结: 缓存导致可见性问题,线程切换带来原子性问题,编译优化带来有序性问题