经过一个案例引出volatile关键字,例如如下代码示例 : 此时没有加volatile关键字两个线程间的通信就会有问题java
public class ThreadsShare { private static boolean runFlag = false; // 此处没有加 volatile public static void main(String[] args) throws InterruptedException { new Thread(() -> { System.out.println("线程一等待执行"); while (!runFlag) { } System.out.println("线程一开始执行"); }).start(); Thread.sleep(1000); new Thread(() -> { System.out.println("线程二开始执行"); runFlag = true; System.out.println("线程二执行完毕"); }).start(); } }
输出结果 :缓存
结论 : 线程一并无感受到线程二已经将 runFlag 改成true 的信号, 因此"线程一开始执行"这句话一直也没有输出,并且程序也没有终结安全
就像下面的场景:多线程
在当前场景中就可能出如今处理器 A 和处理器 B 没有将它们各自的写缓冲区中的数据刷回内存中, 将内存中读取的A = 0、B = 0 进行给X和Y赋值,此时将缓冲区的数据刷入内存,致使了最后结果和实际想要的结果不一致。由于只有将缓冲区的数据刷入到了内存中才叫真正的执行 并发
形成这个问题的缘由:app
计算机在执行程序时,每条指令都是在处理器中执行的。而执行指令过程当中,势必涉及到数据的读取和写入。程序运行过程当中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,因为处理器执行速度很快,而从内存读取数据和向内存写入数据的过程跟处理器执行指令的速度比起来要慢的多,所以若是任什么时候候对数据的操做都要经过和内存的交互来进行,会大大下降指令执行的速度。为了解决这个问题,就设计了CPU高速缓存,每一个线程执行语句时,会先从主存当中读取值,而后复制一份到本地的内存当中,而后进行数据操做,将最新的值刷新到主存当中。这就会形成一种现象缓存不一致ide
针对以上现象提出了缓存一致性协议: MESI优化
核心思想是:MESI协议保证了每一个缓存中使用的共享变量的副本是一致的。当处理器写数据时,若是发现操做的变量是共享变量,即在其余处理器中也存在该变量的副本,会发出信号通知其余处理器将该共享变量的缓存行置为无效状态(总线嗅探机制),所以当其余处理器须要读取这个变量时,发现本身缓存中缓存该变量的缓存行是无效的,那么它就会从内存从新读取。.net
嗅探式的缓存一致性协议:线程
全部内存的传输都发生在一条共享的内存总线上,而全部的处理器都能看到这条总线,缓存自己是独立的,可是内存是共享的。全部的内存访问都要进行仲裁,即同一个指令周期中只有一个处理器能够读写数据。处理器不只在内存传输的时候与内存总线打交道,还会不断的在嗅探总线上发生数据交换跟踪其余缓存在作什么,因此当一个处理器读写内存的时候,其余的处理器都会获得通知(主动通知),他们以此使本身的缓存保存同步。只要某个处理器写内存,其余处理器就会知道这块内存在他们的缓存段中已是无效的了。
MESI详解:
在MESI协议中每一个缓存行有四个状态 :
这里的Invalid,shared,modified都符合嗅探式的缓存一致性协议,可是Exclusive表示独占的,当前数据有效而且和内存中的数据一致,可是只在当前缓存中Exclusive状态解决了一个处理器在读写内存的以前咱们要通知其余处理器这个问题,只有当缓存行处于Exclusive和modified的时候处理器才能写,就是说只有在这两种状态之下,处理器是独占这个缓存行的。
当处理器想写某个缓存行的时候,若是没有控制权就必须先发送一条我要控制权的请求给总线,这个时候会通知其余处理器把他们拥有同一缓存段的拷贝失效,只要在得到控制权的时候处理器才能修改数据,而且此时这个处理器直到这个缓存行只有一份拷贝而且只在它的缓存里,不会有任何冲突,反之若是其余处理器一直想读取这个缓存行,独占或已修改的缓存行必需要先回到共享状态,若是是已经修改的缓存行,还要先将内容回写到内存中
因此 java 提供了一个轻量级的同步机制volatile
volatile是Java提供的一种轻量级的同步机制。volatile是轻量级,由于它不会引发线程上下文的切换和调度。可是volatile 变量的同步性较差,它不能保证一个代码块的同步,并且其使用也更容易出错。volatile关键字 被用来保证可见性,即保证共享变量的内存可见性以解决缓存一致性问题。一旦一个共享变量被 volatile关键字修饰,那么就具有了两层语义:内存可见性和禁止进行指令重排序。在多线程环境下,volatile关键字主要用于及时感知共享变量的修改,并使得其余线程能够当即获得变量的最新值
使用volatile关键字后程序的效果 :
使用方式 :
private volatile static boolean runFlag = false;
代码 :
public class ThreadsShare { private volatile static boolean runFlag = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { System.out.println("线程一等待执行"); while (!runFlag) { } System.out.println("线程一开始执行"); }).start(); Thread.sleep(1000); new Thread(() -> { System.out.println("线程二开始执行"); runFlag = true; System.out.println("线程二执行完毕"); }).start(); } }
输出结果 :
结论 : 线程一感受到了线程二已经将 runFlag 改成true 的信号, 因此"线程一开始执行"这句话获得了输出,并且程序终结了。
volatile 两个效果:
思考 : 若是两个处理器同时读取或者修改同一个共享变量咋办?
多个处理器要访问内存,首先要得到内存总线锁,任什么时候刻只有一个处理器能得到内存总线的控制权,因此不会出现以上状况。
重点 : volatile关键字 被用来保证可见性,即保证共享变量的内存可见性以解决缓存一致性问题
当一个共享变量被volatile修饰时,它会保证修改的值会当即被更新到主存,当有其余线程须要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,由于普通共享变量被修改以后,何时被写入主存是不肯定的,当其余线程去读取时,此时内存中可能仍是原来的旧值,所以没法保证可见性(以上的案例就已经展现了可见性的做用了)
在Java内存模型中,容许编译器和处理器对指令进行重排序,可是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
volatile关键字禁止指令重排序有两层意思:
为了解决处理器重排序致使的内存错误,java编译器在生成指令序列的适当位置插入内存屏障指令,来禁止特定类型的处理器重排序
内存屏障指令 : 内存屏障是volatile语义的实现下面会讲解
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoadBarriers | Load1;LoadLoad;Load2 | Load1数据装载发生在Load2及其全部后续数据装载以前 |
StoreStoreBarriers | Store1;StoreStore;Store2 | Store1数据刷回主存要发生在Store2及其后续全部数据刷回主存以前 |
LoadStoreBarriers | Load1;LoadStore;Store2 | Load1数据装载要发生在Store2及其后续全部数据刷回主存以前 |
StoreLoadBarriers | Store1;StoreLoad;Load2 | Store1数据刷回内存要发生在Load2及其后续全部数据装载以前 |
public class Example { int r = 0; double π = 3.14; volatile boolean flag = false; // volatile 修饰 /** * 数据初始化 */ void dataInit() { r = 1; // 1 flag = true; // 2 } /** * 数据计算 */ void compute() { if(flag){ // 3 System.out.println(π * r * r); //4 } } }
若是线程A 执行 dataInit() ,线程B执行 compute() 根据 happens-before 提供的规则(前一篇java内存模型有讲) java内存模型有讲步骤 2 必定在步骤 3 前面符合volatile规则, 步骤 1 在步骤 2前面,步骤 3 在步骤 4 前面,因此根据传递性规则 步骤 1 也在步骤 4 前面。
当读取一个volatile的变量时会将本地的工做内存变成无效,去内存中获取volatile修饰的变量当前值。
当写一个volatile的变量时会将本地的工做内存中的值强制的刷回内存中。
JMM针对编译器制定的volatile重排序规则表
是否能从新排序 | 第二个操做 | ||
---|---|---|---|
第一个操做 | 普通的读或者写 | volatile读 | volatile写 |
普通的或者写 | NO | ||
volatile 读 | NO | NO | NO |
volatile 写 | NO | NO |
举例说明,第三行最后一个单元格的意思:
当地一个操做为普通操做的时候,若是第二个操做为volatile写,那么编译器不能重排序这两个操做
为了实现volatile的内存语义,编译器在生成字节码的时候,会在指令序列中插入内存屏障来禁止特定类型的处理器排序。
JMM内存屏障插入策略:
volatile写插入内存屏障后生成的指令序列示意图:
StoreStore屏障能够保证在volatile 写以前,其前面的全部普通写操做已经对任意处理器可见了,这是由于StoreStore屏障将保障上面全部的普通写在volatile 写以前刷新到主内存。
StoreLoad屏障能够保证volatile写与后面可能有的volatile读或者写操做重排序。
volatile读插入内存屏障后生成的指令序列示意图:
LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
LoadStore 屏障用来禁止处理器把上面的volatile读与下面的普通读写重排序。
实际上,这些条件代表能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上,上面的两个条件就是保证对该volatile变量的操做是原子操做,这样才能保证使用 volatile关键字的程序在并发时可以正确执行
在多线程环境下及时感知共享变量的修改,并使得其余线程能够当即获得变量的最新值
场景一 : 状态标记量(文中举例)
public class ThreadsShare { private volatile static boolean runFlag = false; // 状态标记 public static void main(String[] args) throws InterruptedException { new Thread(() -> { System.out.println("线程一等待执行"); while (!runFlag) { } System.out.println("线程一开始执行"); }).start(); Thread.sleep(1000); new Thread(() -> { System.out.println("线程二开始执行"); runFlag = true; System.out.println("线程二执行完毕"); }).start(); } }
场景二 Double-Check
DCL版单例模式是double check lock 的缩写,中文名叫双端检索机制。所谓双端检索,就是在加锁前和加锁后都用进行一次判断
public class Singleton1 { private static Singleton1 singleton1 = null; private Singleton1 (){ System.out.println("构造方法被执行....."); } public static Singleton1 getInstance(){ if (singleton1 == null){ // 第一次check synchronized (Singleton1.class){ if (singleton1 == null) // 第二次check singleton1 = new Singleton1(); } } return singleton1 ; } }
用synchronized只锁住建立实例那部分代码,而不是整个方法。在加锁前和加锁后都进行了判断,这就叫双端检索机制。这样确实只建立了一个对象。可是,这也并不是绝对安全。new 一个对象也是分三步的:
步骤二和步骤三不存在数据依赖,所以编译器优化时容许这两句颠倒顺序。当指令重排后,多线程去访问也会出问题。因此便有了以下的最终版单例模式。这种状况不会发生指令重排
public class Singleton2 { private static volatile Singleton2 singleton2 = null; private Singleton2() { System.out.println("构造方法被执行......"); } public static Singleton2 getInstance() { if (singleton2 == null) { // 第一次check synchronized (Singleton2.class) { if (singleton2 == null) // 第二次check singleton2 = new Singleton2(); } } return singleton2; } }