文章目录
- 在物理计算机中CPU为了提高处理速度,添加了高速缓存与CPU乱序执行
一、 并发的起源
为了提高计算机处理数据的速度,现代的计算机都支持多任务处理。
在32位windows操作系统中 ,多任务处理是指系统可同时运行多个进程,而每个进程也可同时执行多个线程。一个线程是指程序的一条执行路径,它在系统指定的时间片中完成特定的功能。系统不停地在多个线程之间切换,由于时间很短,看上去多个线程在同时运行。或者对于在线程序可并行执行同时服务于多个用户称为多任务处理。
在本篇中我们就主要讲解:使用 [高速缓存] 和 [乱序执行] 来提高CPU(处理器)的数据处理速度,所引发的问题。
二、物理计算机的内存模型
理解java内存模型之前,我们先来了解一下,物理计算机的内存模型,其对Java内存模型有着很大的参考意义。
在物理计算机中:
- 我们需要处理的数据都在内存中
- 处理器处理数据,需要从内存中获取相应的数据,然后存入内存中。
为了提高计算机的处理速度(读取数据,存储数据有IO消耗),我们常常会在CPU(处理器)中加入高速缓存(Cache Memory)。
高速缓存(Cache Memory)是位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。
高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先缓存中调用,从而加快读取速度。
高速缓存也就是指将数据缓存到处理器中,当处理器处理完数据后,再将处理的数据结果存储在内存中。具体如下图所示:
当CPU(处理器)要读取一个数据时:
- 首先从一级缓存中查找
- 如果没有找到再从二级缓存中查找
- 如果还是没有就从三级缓存或内存中查找。
一般来说,每级缓存的命中率大概都在80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取。
三、缓存结构在多线程模型中引发的缓存不一致问题
虽然高速缓缓冲提高了CPU(处理器)处理数据的速度问题,但是其在多线程中运行就会有问题了。
在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。这时CPU缓存中的值可能和缓存中的值不一样,这就会出现缓存不一致的问题。
为了解决该问题。物理机算计提供了两种方案来解决该问题。具体如下图所示:
【图缓存结构在多线程模型中引发的缓存不一致问题】
3.1 通过总线加LOCK#锁的方式
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束,在计算机中数据是通过总线,在处理器和内存之间传递。
【缓存不一致:通过总线加LOCK#锁的方式】
因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。
在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从其内存读取变量,然后进行相应的操作。
这样就解决了缓存不一致的问题。
3.2 通过缓存一致性协议的方式
对于[总线加LOCK#锁]的方式,由于在锁住总线期间,其他CPU无法访问内存,会导致效率低下。因此出现了第二种解决方案:通过缓存一致性协议来解决缓存一致性问题。
最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
它核心的思想是:
- 当CPU写数据时,如果发现操作的变量是共享变量(即在其他CPU中也存在该变量的副本),会发出信号通知其他CPU将该变量的缓存行置为无效状态
- 因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
四、CPU(处理器)的乱序执行(out-of-orderexecution)
除了使用高速缓存来提高CPU(处理器)的数据处理速度,CPU(处理器)还采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术。 在这期间:
- 不按规定顺序的执行指令
- 然后由重新排列单元将各执行单元结果按指令顺序重新排列。
采用乱序执行技术的目的是为了使CPU内部电路满负荷运转并相应提高了CPU的运行程序的速度。下面这个例子帮助大家理解:
【乱序执行:顺序执行示例】
假如请A、B、C三个名人为晚会题写横幅“春节联欢晚会”六个大字,每人各写两个字。如果这时在一张大纸上按顺序由A写好"春节"后再交给B写"联欢",然后再由C写"晚会",那么这样在A写的时候,B和C必须等待,而在B写的时候C仍然要等待而A已经没事了。
【乱序执行:乱序执行示例】
但如果采用三个人分别用三张纸同时写的做法, 那么B和C都不必须等待就可以同时各写各的了,甚至C和B还可以比A先写好也没关系(就象乱序执行)。当他们都写完后就必须重新在横幅上(自然可以由别人做,就象CPU中乱序执行后的重新排列单元)按"春节联欢晚会"的顺序排好才能挂出去。
乱序执行是指单个线程中的顺序代码可能会乱序执行
虽然引入了乱序执行来提高cpu的效率,但是CPU不会对任务操作进行重排序,编译器与处理器只会对没有数据依赖性的指令进行重排序。
这里提到了一个关键词数据依赖性。什么是数据依赖呢?
4.1 数据依赖
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a=1;b=a | 写一个变量之后,再读这个位置 |
写后写 | a=1;a=2 | 写一个变量之后,再写这个位置 |
读后写 | a=b;b=1 | 读一个变量之后,再写这个位置 |
上述三种情况,a与b存在着“数据依赖性”
同时大家也要注意:这里所说的数据依赖性是指**单个处理器执行的指令序列和单个线程中执行的操作,多处理器和不同线程之间是没有数据依赖性这种关系的**。
4.2 重排序规则(as-if-serial)
既然我们已经知道了CPU在处理数据时候会出现重排序。那重排序的规则是什么呢?
重排序规则:不管怎么重排序(编译器和处理器为了提高并行度),单线程(程序)执行结果不能被改变。(当然,多线程中还是无法保证,后文会讲到)
编译器、runtime和处理器都必须遵守。那么我们三角形面积示例代码说明:
double a = 3;//底
double h = 10;//高
double s = a*h/2//面积
a与s存在数据依赖关系,同时h与s也存在依赖关系。
因此在程序的最终指令执行时: s是不能排在a与h之前。
因为a与h不存在着数据依赖关系。所以处理器可以对a与h之前的执行顺序重排序。
【重排序规则:三角面积】
经过处理器的重排序后,执行的结果并没有发生改变。
【重排序规则:三角面积2】