第一次写文章,主要作为自己以后复习用的笔记,水平有限,有不对的地方,还望大佬指正,谢谢!
话不多说,要理解volatile关键字,还要从硬件层面开始讲起。
操作系统层面
随着现代操作系统的CPU访问速度越来越快,和主存速度的差异越来越大,所以在主存和CPU之间加了一层高速缓存,也就是L1、L2、L3缓存,先从主存读取数据,放到高速缓存,之后CPU再读取数据就可以直接从高速缓存读取了,由此加快访问速度。
由此引发的问题
由于上述硬件层面加了一层高速缓存,由此会引发缓存一致性问题,如果一个变量被多个CPU共享,其中CPU0在自己的高速缓存修改了该共享变量,还没有同步到主存,此时CPU1从主存中取到的是旧值,使用旧值进行操作,就会导致数据出错。
所以,硬件层面使用总线锁和缓存锁来解决这一问题,总线锁即锁住总线,多个CPU串行访问主存,CPU0对变量操作期间,CPU1不能访问。但是这会导致性能下降,多核CPU的优势并没有体现出来,所以后来的操作系统,大多使用缓存锁,即锁住缓存行(内存中存储数据是以缓存行为单位),一个缓存行默认是64byte,如果多个CPU访问的是同一个缓存行,才会被阻塞,否则不会影响。(这里会有伪共享问题,即一个缓存行存储多个变量,即使多个CPU访问的不是同一个变量,也会被阻塞,可以使用对齐填充来解决)
MESI协议
硬件层面使用缓存一致性协议,常用的是MESI协议,该协议主要有两方面的作用:
1.多个CPU共享同一变量,CPU0修改该变量,会将CPU1中的高速缓存中该变量的缓存行置为失效
2.如果CPU的该变量的缓存行是失效状态,需要从注册重新读取最新数据
JVM层面
虽然硬件层面可以解决,但并不是所有的操作系统都支持缓存一致性协议,为了屏蔽各个硬件底层的差异,JVM层面抽象出来了JMM内存模型。
每个线程有自己私有的工作内存,共享变量初始在存在主内存中,线程从主内存取出放到自己的工作内存中,之后就从工作内存中读取。
共享变量多线程安全问题
由于JMM模型,同操作系统硬件的高速缓存类似,也会存在数据不一致,Java层面使用volatile实现对共享变量多线程操作下的线程安全问题,可以实现内存可见性、有序性。
为了实现程序的高效运行,编译器、处理器、内存都会对指令进行重排序,volatile依赖MESI协议和内存屏障来实现线程安全,其中内存屏障是使用Lock汇编指令,防止指令前后重排序。但是volatile不能实现原子性,比如i++,分为三步,取出i的值,对i+1,将计算后的结果赋值给i,如果线程1在完成第2步后,此时还没有修改i变量值,即还没有使其他CPU核心的高速缓存的缓存行失效,线程2从自己的工作内存取出i值进行操作,然后线程1才将计算后的i值赋值为i,并将其他CPU核心的高速缓存的缓存行失效,但是这时已经晚了,线程2已经取了旧值。如果是原子操作,例如i=1,线程1直接将i值赋为1,并将其他CPU核心的高速缓存的缓存行失效,将新值写入主存,此时线程2取i值的话发现自己的缓存行失效,就从主存取,也就不会出现线程安全问题了。
经典的DCL单例模式
加双重if,是为了防止当多个线程并发同时进入第一个if,然后第一个线程获取到锁以后,创建实例,当第一个线程释放锁时,第二个线程获取到锁,再次判断已经不为null了,就会直接返回。synchronized代码块,和代码块外部的代码不能重排序,是happens-before保证的,即一个锁的释放(同步块或者方法的退出)和该锁的后续获得(同步块或者方法的进入)存在happens-before关系,但是synchronized代码块内,是可以重排序的,也即new Single()分为三个步骤:
1.划分内存区域
2.填充实例
3.将栈里面的引用变量指向对象内存区域
重排序后,可能132乱序执行,此时instance!=null,但是实例数据还没有填充,返回的其实是个空实例,所以加上volatile,禁止指令重排序,且创建完实例后,立即刷到主存,其他线程可以立刻感知到。
volatile在JDK源码中的运用
最常用的并发包里的底层基础类,AbstractQueuedSynchronizer,里面有一个state变量
该变量表示锁的重入次数,被volatile修饰,线程每次争抢到锁后,对state+1,其他线程立即可见,在争抢锁时,先判断state,如果等于0,说明锁没有被占用,代表可以争抢,用CAS设置state,设置成功,表示成功获取锁;如果state不等于0,说明锁已经被占用,再判断获取锁的线程是否当前线程,如果是,将state+1,表示重入。
总结
1.volatile用于修饰共享变量
2.volatile底层依赖MESI协议和内存屏障,实现多线程并发下的内存可见性、有序性,但不能实现原子性
3.volatile除了在JDK并发包下基础类AQS中有应用,在其他很多组件源码中也有应用