volatile实现原理

volatile简介:

在Java的多线程中,允许线程访问共享变量,但是为了保证共享变量被准确,一致的更新,需要添加排它锁确保每次更新只有一个线程单独获取它。在某些情况下,Java提供了volatile,当一个变量被声明为volatile时,JVM会确保所有的线程看到的值是一致的。volatile的实现与计算机的底层设计密切相关,要了解volatile的实现原理,需要先了解CPU对共享数据的处理方式。

CPU的内存屏障(memory barriers)

简介:CPU在运行时为了提高处理的效率存在着指令的重排序,利用内存屏障可以保证某些指令的执行顺序可以和我们预计的一致。

四种内存屏障:

1.loadload:

示例:load1 loadload load2(load1,load2代表了两个读取内存数据的指令)表示load2指令的执行在load1指令执行之后

2.storestore:

示例:store1 storestore store2(store1,store2代表了两个向内存写入数据的指令)表示store2指令的写入需要在store1之后,保其他处理器对store1写入的可见性

3.loadstore:

示例:load1 loadstore store1 表示store1在写入时保证load1读取数据完毕

4.storeload:

示例:store1 storeload load1 表示在load1指令执行前store1对内存写入的数据对所有处理器可见。该内存屏障被大多数处理器支持,storeload会确保所有在该内存屏障之前的内存访问指令执行完成后才执行该内存屏障之后的指令。

CPU Cache

简述:随着计算机技术的发展,CPU计算速率越来越快,内存的读取速度相对CPU而言变得愈发的慢了,为了提高计算机的性能在CPU和内存之间引入了高速缓存(CPU Cache),CPU Cache有三个等级:L1,L2,L3,缓存的大小依次变大,而速度却是依次变慢的。其中L1,L2是CPU中每个核独有的,每个CPU只有一个L3为CPU中核共享的内存空间(对于不同类型的CPU而言,缓存级数可能并不一致,某些类型的CPU仅有两级缓存)

缓冲行(cache line)

CPU的高速缓存被分为了多个组,每个组又分为了多个行,这个行就是cache line,每个cache line的大小是固定的,其中利用64字节的空间缓存数据,缓存的数据是主内存中数据的拷贝。cache line还有一个有效位(valid bit)和多个标记位(tag bit),valid bit用来表示当前cache line是否有效,tag bit表示cache line中缓存数据在主内存中的地址信息,这是由于CPU在查询数据时都是根据物理地址来寻找的。值得一提的是CPU操作数据的基本单位不是变量,而是以cache line为基本单位进行操作。如果cache line中的一个变量被修改,其余处理器中对应的cache line都会被置为无效。

缓存命中(cache hit)

当程序运行时CPU需要访问的内存数据已经被加载到了cache line中时,称之为缓存命中,如果在cache line中没有缓存CPU需要的数据称为cache line miss,此时CPU需要重新发送加载内存数据的指令,而内存的速度相对于CPU而言是比较慢的,CPU不得不等待数据完成加载,程序的性能被降低了。为此我们需要提高cache line缓存命中率,可以根据时间局部性和空间局部性原理对其进行提高。

时间局部性:一个数据可能被多次使用,当其被加载到cache line后,此后的每次使用都会命中,以此提高数据的读取效率。

空间局部性:一个cache line有64字节的存储空间,我们可以充分利用这个空间,一次将后面需要用到的数据加载到cache line中,而不是重新寻址,以此提高命中率。

缓存行填充(cache line fill)

在介绍缓存行填充前需要注意,在L1中的所有cache line同时存在于L2中,L2中的所有cache line同时存在于L3中,当CPU处理完数据后需要装载新的数据时会先将L1中的数据逐出,并推入到L2中,依次类推,最终将数据写回到主内存中。如下图,当CPU中的core1先对cache line的共享数据进行修改时,会同时通知core2,core2检测到了一个写指令,并且拥有一个cache line的原始副本,那么core2将会将cache line中的valid bit置为无效,而在core2接下来对给cache进行操作时会重新加载该cache line。由此可见对于同时修改一个cache line而言,core1和core2的并行操作变为了一个串行操作,在该示例中core1和core2操作的虽是同一个cache line但是操作的数据并不一样,如果需要提高程序的效率就需要使用缓存行填充。

在使用缓存填充时需要区分哪些数据是会相互影响的,哪些数据是相互独立的,哪些数据是不变的,将这些数据放在不同的缓冲行中,缓冲行空余的部分可以用空的变量进行填充,例如Java中使用long的变量对其进行填充,这样原本可能存储在同一个cache line中的数据就会被分配多个cache line中,以上图示例为例,core2将不用再等待core1将cache line写回到主内存中后再重新加载,而是直接就可以对Y进行修改,从而提高程序运行效率。

Java中使用缓存填充:JDK1.8对缓存填充做了支持,可以利用@Contended使得各个变量在cache line中分隔开,除此之外还需要设置一个JVM参数:-XX:-RestrictContended

volatile的实现原则:

前面提到,当一个变量被声明为volatile时,Java内存模型会保证所有线程看到的该变量的值是一致的。当被声明为volatile的变量被修改时,JVM会发出一个lock指令给CUP,当CPU执行该指令时,会将缓存中的数据写入到内存中,此时,其他处理器会将缓存有该内存地址对应数据的cache line置为无效。该过程涉及到了MESI(一种缓存一致性协议)。

参考资料:

https://blog.csdn.net/qq_27680317/article/details/78486220

http://blog.jobbole.com/36263/

展开阅读全文

没有更多推荐了,返回首页