我的并发编程(三):Volatile的底层实现及原理
一、概述
Java语言中要学习同步和多线程,必定绕不开Volatile关键字,它定义为Java允许线程访问共享变量,为了确保共享变量能够被准确和一致性地更新,线程应该确保通过排它锁单独获得这个变量。那么今天我们来深入解析一下Volatile的底层实现及原理。
二、详细分析
1. 程序在计算机中如何执行的?
计算机的组成如下图所示:
当我们需要执行一个程序时,分为以下几步:
(1) 将程序从磁盘中加载到内存中做成一个进程,进程里面有一条一条的指令,指令旁是数据;
(2) CPU要执行的时候,先将指令读取到CPU里,然后将指令用到的数据读取到CPU中的寄存器内;
(3) 执行完指令后,再将得出的结果写回到内存中。
2. cache line缓存行
cache line的概念以及缓存行对齐和伪共享模型图如下:
我们都知道,CPU的运行速度非常快,而内存的速度非常慢,所以引入缓存的概念来提高速度进而提升效率,如上图所示,L1、L2、L3都是缓存。目前的CPU架构一般是3级缓存,L1、L2都处于CPU内部,所以速度非常快。我们系统在读数据的时候,是分块读取的,这样做的原理是我们在读取一个数据后,也许很快就会用到旁边的数据,分块读取就减少的读取次数,也减少了读取带来的消耗。
上述CPU分块读取数据是怎么做的呢?CPU多数的实现是从内存里一次性读64个字节到CPU的缓存中,这块数据就叫做缓存行,也就是内存读取数据到CPU缓存的基本单位。这个和操作系统64位有一定的关系,操作系统64位一味这一次可以读8个字节,而现在的CPU缓存空间足够,就一次读取64个字节。
缓存行越大,局部性空间效率越高,但读取时间越慢; 缓存行越小,局部性空间效率越低,但读取时间越快;上诉的64个字节是时间和空间效率上做的折中选择。
3. 缓存一致性协议
现在的CPU基本都是多核CPU,服务器更是提供了多CPU的支持,而每个核心也都有自己独立的缓存,当多个核心同时操作多个线程对同一个数据进行更新时,如果核心2在核心1还未将更新的数据刷回内存之前读取了数据,并进行操作,就会造成程序的执行结果造成随机性的影响。 因此我们引入了缓存一致性协议来对内存数据的读写进行管理。
缓存一致性协议有MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等等,但是由于Intel的CPU使用的最多的是MESI,所以接下来我们主要介绍MESI协议。
MESI分别代表缓存行数据所处的Modify(修改)、Exclusive(独占)、Shared(共享)、Invalid(失效)这四种状态,通过对这四种状态的切换,来达到对缓存数据进行管理的目的。
那么系统底层是如何实现数据一致性呢?一般情况下是使用缓存一致性协议MESI,但是当MESI无法解决时,就需要对系统总线进行加锁。
4. 缓存行对齐
由缓存一致性协议引发的一个有趣的问题,当Thread1和Thread2需要同时修改同一缓存行中不同的变量,会出现交替通知缓存行失效的情况,进而导致效率变低。解决的办法就是让两个变量处于不同的缓存行中,具体代码如下:
(1) 处于同一缓存行时:
(2) 处于不同缓存行:
虽然都是对一个long型数组的两个值在两个线程中修改,但是第二种会比第一种效率高,就是利用了缓存行对齐的原理。著名的开源并发框架Disruptor(可以用来替换java中的阻塞队列ArrayBlockingQueue)的内部实现中,有一个环形队列Ring Buffer,也叫环形缓冲区,部分源码如下:
Ring Buffer环形缓冲区有一个指针,名为cursor,源码中,在定义cursor前后各放了7个long型变量,也就是56个字节,这样使得cursor不会与其他被volatile修饰的变量处于同一缓存行,所以Disruptor号称单机最快的缓存队列。
5. CPU的乱序执行
CPU中有个乱序执行的概念,概念图如下:
CPU在执行指令的时候,往往不是顺序执行,但是会遵守as-if-serial原则,也就是最终一致性原则。虽然指令执行顺序发生改变,但是不会影响单线程执行结果。多线程情况下为了不让CUP进行指令重排序,则需要用到Volatile关键字,因为Volatile的重要作用之一就是防止指令重排序。
6. 内存屏障
内存屏障其实就是一个CPU指令,在硬件层面上来说可以扥为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。主要有两个作用:
(1) 阻止屏障两侧的指令重排序;
(2) 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
在JVM层面上来说作用与上面的一样有一个JSR内存屏障,种类可以分为四种:
java底层实现Volatile的禁止指令重排序,也就是JVM会遵循自身的原则,对不同情况的指令添加不同类型的JSR内存屏障。
上述只是再JVM中的实现,那么在系统底层是如何实现的呢?现在大多数的CPU都在底层有指令支持内存屏障的,一共有3条CPU原语( 所谓原语,一般是指由若干条指令组成的程序段,用来实现某个特定功能,在执行过程中不可被中断),如下图:
那么那些没有CPU原语来支持内存屏障的CPU又怎么实现呢,那就需要对总线进行加锁来实现。所以JVM中的内存屏障在不同的CUP中的实现方式是不一样的,一般是CPU原语支持,特殊情况下就是锁总线。
7. 为什么Volatile不能保证原子性?
下图是一个操作系统内存屏障的汇编指令:
Volatile在编译成汇编后,我们可以看到jvm的内存屏障在操作系统底层是lock addl指令,但是这条指令是添加一个0值到rsp寄存器中,任何加0的操作都不会影响原来的值,也就是说lock后面跟了一条无意义的指令。这里这么做就是为了加lock,但是lock指令后面必须跟一条指令,所以加了一条无意义的指令。因此,Volatile并不像synchronized那样后面跟的是一个cmpxchg原语锁CAS操作,所以Volatile显得轻量级一些,也导致Volatile并不能保证原子性。
三、总结
通过本文,不但了解了CPU层面的缓存以及指令调用,也更加深刻的理解了Volatile关键字的底层实现及原理。其中Volatile最关键的特点就是以下三点:
(1) 线程可见性;
(2) 禁止指令重排序;
(3) 不能保证原子性。
更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!
本文地址:https://blog.csdn.net/qq_34942272/article/details/107566279
上一篇: Spring Boot整合Shiro
下一篇: weka中朴素贝叶斯的实现