java中外部锁实现就用到了自旋锁这个概念,以前看jdk的concurrent包源码的时候,对这部分实现一直是没法透彻的理解。正所谓”外行看热闹,内行看门道“。虽然代码按行看都能看懂,可是连在一块儿不知道是为何这么作。只能对Doug Lea大神佩服的十体投地。这么复杂精巧的代码是怎么想出来的? java
后面看了《多处理器编程的艺术》这本书时,对自旋的概念开始有了一点概念。要想了解并应用好自旋锁,不只仅是对相关算法的了解,还须要一些些底层硬件的知识哦。不过不用担忧,须要的底层硬件知识不是不少(万幸),因此在详细讲解各个自旋锁以前,须要对这些一点点的硬件知识了解一下,正好以此做为开篇。这个系列大部份内容和代码都来自于《多处理器编程的艺术》一书,算是一个读书笔记吧。 程序员
一个多处理器是由多个硬件处理器组成。其中每个处理器都能执行一个顺序程序。处理器提取和执行一条指令的时间叫作时钟周期,这也是咱们用来衡量程序执行性能的基本时间单位。处理器能够执行线程一段时间,而后不去管这个线程有没有执行完,转而去执行另外一个线程。这个切换过程就是咱们熟悉的上下文切换。处理器会由于各类缘由从调度中删除一个线程去执行其余的线程。在多处理器系统中,当线程从一个处理器调度中被删除后,可能从新在另外一个处理器上执行。 算法
互连线是cpu和内存以及cpu和cpu之间进行通讯的一种媒介。有两种基本的链接模式:SMP(对称duochuli)和 编程
NUMA(非一致性内存访问)。 数组
在SMP系统结构中,处理器和存储器经过总线来通讯。处理器和存储器都有用来负责发送和监听总线上广播信息的总线控制单元。SMP系统结构很是广泛,由于它们比较容易构建。可是对于处理器数量较多的系统来讲,这种结构不利于扩展,由于最终总线会成为瓶颈。 缓存
NUMA中,一系列节点经过点对点网络链接,就像一个小的局域网。每一个节点包含一个或多个处理器和一个存储器。一个节点的存储器对于其余节点来讲是能够访问的。全部节点的本地存储器组成了一个全部处理器共享的全局存储器。NUMA名字叫作非一致性内存访问,意思就是处理器访问本身的本地存储要比访问其余处理器的存储要快。访问内存的速度并非一致的。NUMA结构显然比较复杂,须要的协议也更复杂。可是对于处理器数量多的系统来讲扩展性更好。 网络
能够在SMP和NUMA之间找一个平衡。每一个节点用SMP来构造,而链接节点则采用NUMA结构。固然对于咱们程序员来讲,咱们只要知道一点:互连线是由处理器共享的有限资源。若是一个处理器占用了较多的互连线资源,其余的处理器必然会被延时执行。这一点很是重要。 性能
主存能够当作一个由全部处理器共享的由字组成的一个大数组。咱们经过特定的地址来访问主存对应的区域。一般字长是32字节或者64字节。字长为32字节的系统主存地址是32位的,字长为64字节的系统主存地址是64位的。也就是咱们一般说的32位机和64位机。处理器经过链接线给主存发送包含目的地址的信息,用来获取主存中对应目的地址的值。或者发送包含目的地址以及一个新的数据,用于向主存对应的地址中中写入新值,当新值被写入后,主存会发送确认信息。咱们能够看到,处理器对主存的读取和写入都会占用互连线资源。 测试
处理器对主存的一次访问可能会花费数百个时钟周期,若是处理器频繁的对主存进行读取操做意味着处理器将会花费大量的时间等待主存响应请求。另外,处理器访问主存会占用互连线资源,形成其余的处理器的延迟。因此高速缓存就出现了,这是一个介于处理器和主存之间的一个小容量存储器。高速缓存的读取速度比主存要快的多。当处理器要读取一个值时,首先会到高速缓存中去寻找,若是存在,处理器就不用再去访问比较慢的主存了,不然处理器必须还要到主存中去取值。咱们把读取的值在高速缓存中存在这种状况叫作”cache 命中“,不存在这种状况叫作”cache缺失“。理解”cache命中“和”cache缺失“对设计高性能的自旋锁但是很是必要的哦。 spa
当一个处理器读或写了被另外一个处理器装入高速缓存的主存地址时,将发生共享。例如处理器A读写了主存的一个值,并装入本身的高速缓存。处理器B也在随后读取了同一个值,此时会发生共享(或者叫内存争用)。若是两个处理器共享同一个主存地址,一个处理器修改了改地址的值,另外一个处理器的高速缓存中保存的值将会被做废,以确保不会读到过时值。这个问题就是缓存的一致性问题。
有一种最经常使用的叫作MESI的协议用于解决缓存一致性问题,下面来详细了解一下。
首先对缓存块的状态进行命名:
咱们用例子来解释一下MESI协议:
(1)刚开始,处理器A读取主存中的数据a并储存在高速缓存中。此时A的缓存块对应的状态是Exclusive。这个不用解释。
(2)而后处理器B也读取了相同的数据a也缓存到了本身的高速缓存中。此时A和B共享主存中的同一个数据a。因此它们的缓存块状态都是shared。
(3)接着,处理器B修改了数据a为b,可是只是修改了缓存块中的数据,并无同步到主存中去。此时B的缓存块状态是modified,同时A的缓存块状态变为了invalid。B在修改的同时回向其余处理器广播,因此A修改了本身的缓存块状态。
(4)若是A此时要从读取a时,会广播请求。B收到请求将修改后的值同时发送给处理器A和主存,并将A和B的缓存块状态变成和谐的shared状态。
其实这个MESI协议仍是比较好理解的。
若是处理器不断的测试内存中的某个字,并等待另外一个处理器处理它,则称该处理器正在自旋。举例来讲,内存中存在一个布尔变量a,初始值为false。A处理器不断地去读取它,知道a被另外一个处理器设置成true为止。咱们称处理器A的行为(不断的读取a,并测试a是否是为true)叫作自旋。其实也就是名字唬人罢了。
对于没有高速缓存的SMP系统结构来讲,自旋是一种很是糟糕的想法。由于,每次自旋都是处理器到主存中去读取值。每次的读取操做都会消耗总线资源(还记得上面SMP的结构图吗),会直接影响到其余处理器的推动。
对于无高速缓存的NUMA系统结构来讲(NUMA也能够带高速缓存的)。若是自旋的地址位于本地存储器中,这个是能够接受的。不然也是糟糕的想法。还好目前不带高速缓存的多处理器系统结构不多见。
对于有高速缓存的SMP和NUMA系统结构来讲,自旋仅消耗很是少的资源。由于处理器第一次读取肯会直接到主存中去读(cache缺失),但后面自旋过程当中,只要数据没有改变,处理器都会从本身的高速缓存中去读取数据(cache命中)。这种咱们称为”本地自旋“。一旦高速缓存中的数据被改变,马上会产生一个cache缺失(缓存块状态为invalid),既然数据已经被改变,自旋也会随之中止。