缓存不一致性问题
为了解决缓存不一致性问题,通常来说有以下2种解决办法:
1)通过在总线加LOCK#锁的形式
2)通过缓存一致性协定
这2种形式都是硬件层面上提供的形式。
在晚期的CPU当中,是通过在总线上加LOCK#锁的模式来解决缓存不统一的问题。因为CPU和其余部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其余CPU对其余部件拜访(如内存),从而使得只能有一个CPU能应用这个变量的内存。比方如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上收回了LCOK#锁的信号,那么只有期待这段代码齐全执行结束之后,其余CPU能力从变量i所在的内存读取变量,而后进行相应的操作。这样就解决了缓存不统一的问题。
然而下面的形式会有一个问题,因为在锁住总线期间,其余CPU无法访问内存,导致效率低下。
所以就呈现了缓存一致性协定。最闻名的就是Intel 的MESI协定,MESI协定保障了每个缓存中应用的共享变量的正本是统一的。它外围的思维是:当CPU写数据时,如果发现操作的变量是共享变量,即在其余CPU中也存在该变量的正本,会发出信号告诉其余CPU将该变量的缓存行置为有效状态,因而当其余CPU须要读取这个变量时,发现自己缓存中缓存该变量的缓存行是有效的,那么它就会从内存从新读取。
volatile的内存语义
volatile能够保障线程可见性且提供了肯定的有序性,然而无奈保障原子性。在JVM底层volatile是采纳“内存屏障”来实现的。
1.volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的动态成员变量)被volatile润饰之后,那么就具备了两层语义:
1)保障了不同线程对这个变量进行操作时的可见性,即一个线程批改了某个变量的值,这新值对其余线程来说是立刻可见的。
2)禁止进行指令重排序。
先看一段代码,如果线程1先执行,线程2后执行:
//线程1boolean stop = false;while(!stop){ doSomething();} //线程2stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采纳这种标记方法。然而事实上,这段代码会齐全运行正确么?即肯定会将线程中断么?不肯定,兴许在大多数时候,这个代码可能把线程中断,然而也有可能会导致无奈中断线程(尽管这个可能性很小,然而只有一旦产生这种状况就会造成死循环了)。
上面解释一下这段代码为何有可能导致无奈中断线程。在后面曾经解释过,每个线程在运行过程中都有本人的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在本人的工作内存当中。
那么当线程2更改了stop变量的值之后,然而还没来得及写入主存当中,线程2转去做其余事件了,那么线程1因为不晓得线程2对stop变量的更改,因而还会始终循环上来。
然而用volatile润饰之后就变得不一样了:
第一:应用volatile关键字会强制将批改的值立刻写入主存;
第二:应用volatile关键字的话,当线程2进行批改时,会导致线程1的工作内存中缓存变量stop的缓存行有效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行有效);
第三:因为线程1的工作内存中缓存变量stop的缓存行有效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2批改stop值时(当然这里包含2个操作,批改线程2工作内存中的值,而后将批改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行有效,而后线程1读取时,发现自己的缓存行有效,它会期待缓存行对应的主存地址被更新之后,而后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
察看退出volatile关键字和没有退出volatile关键字时所生成的汇编代码发现,退出volatile关键字时,会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。内存屏障是一组解决指令,用来实现对内存操作的程序限度。volatile的底层就是通过内存屏障来实现的。
2. volatile能保障有序性吗?
在后面提到volatile关键字能禁止指令重排序,所以volatile能在肯定水平上保障有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其后面的操作的更改必定全副曾经进行,且后果曾经对前面的操作可见;在其前面的操作必定还没有进行;
2)在进行指令优化时,不能将在对volatile变量拜访的语句放在其前面执行,也不能把volatile变量前面的语句放到其后面执行。
举个简略的例子:
//x、y为非volatile变量//flag为volatile变量 x = 2; //语句1y = 0; //语句2flag = true; //语句3x = 4; //语句4y = -1; //语句5
因为flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2后面,也不会讲语句3放到语句4、语句5前面。然而要留神语句1和语句2的程序、语句4和语句5的程序是不作任何保障的。
并且volatile关键字能保障,执行到语句3时,语句1和语句2必然是执行结束了的,且语句1和语句2的执行后果对语句3、语句4、语句5是可见的。
3.volatile的原理和实现机制
后面讲述了源于volatile关键字的一些应用,上面咱们来探讨一下volatile到底如何保障可见性和禁止指令重排序的。
上面这段话摘自《深刻了解Java虚拟机》:
“察看退出volatile关键字和没有退出volatile关键字时所生成的汇编代码发现,退出volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个性能:
1)它确保指令重排序时不会把其前面的指令排到内存屏障之前的地位,也不会把后面的指令排到内存屏障的前面;即在执行到内存屏障这句指令时,在它后面的操作曾经全副实现;
2)它会强制将对缓存的批改操作立刻写入主存;
3)如果是写操作,它会导致其余CPU中对应的缓存行有效。
应用volatile关键字的场景
synchronized关键字是避免多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些状况下性能要优于synchronized,然而要留神volatile关键字是无奈代替synchronized关键字的,因为volatile关键字无奈保障操作的原子性。通常来说,应用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于以后值
2)该变量没有蕴含在具备其余变量的不变式中
实际上,这些条件表明,能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包含变量的以后状态。
事实上,我的了解就是下面的2个条件须要保障操作是原子性操作,能力保障应用volatile关键字的程序在并发时可能正确执行。
上面列举几个Java中应用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;}volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);
2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; }}
volatile的内存语义实现
为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优安排来最小化插入屏障的总数简直是不可能的,为此,JMM采取了激进策略:
- 在每个volatile写操作的后面插入一个StoreStore屏障;
- 在每个volatile写操作的前面插入一个StoreLoad屏障;
- 在每个volatile读操作的前面插入一个LoadLoad屏障;
- 在每个volatile读操作的前面插入一个LoadStore屏障。
须要留神的是:volatile写是在后面和前面别离插入内存屏障,而volatile读操作是在前面插入两个内存屏障
StoreStore屏障:禁止下面的一般写和上面的volatile写重排序;
StoreLoad屏障:避免下面的volatile写与上面可能有的volatile读/写重排序
LoadLoad屏障:禁止上面所有的一般读操作和下面的volatile读重排序
LoadStore屏障:禁止上面所有的一般写操作和下面的volatile读重排序