zoukankan      html  css  js  c++  java
  • Java内存模型-volatile的内存语义

    一 引言

      听说在Java 5之前volatile关键字备受争议,所以本文也不讨论1.5版本之前的volatile。本文主要针对1.5后即JSR-133针对volatile做了强化后的了解。

    二 volatile的特性

      开门见山,volatile变量自身具有以下特性:

    • 可见性(最重要的特性)。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
    • 原子性。对任意(包括64位long类型和double类型)单个volatile变量的读/写具有原子性。但是类型于a++这种复合操作不具有原子性。

      下面通过案例来证明下可见性,先看一个普通变量是否能保证可见性:

     3 class Example {
     4     private boolean stop = false;
     5     public void execute() {
     6         int i = 0;
     7         System.out.println("thread1 start loop.");
     8         while(!getStop()) {
     9             i++;
    10         }
    11         System.out.println("thread1 finish loop,i=" + i);
    12     }
    13     public boolean getStop() {
    14         return stop; // 对普通变量的读
    15     }
    16     public void setStop(boolean flag) {
    17         this.stop = flag; // 对普通变量的写
    18     }
    19 }
    20 public class VolatileExample {
    21     public static void main(String[] args) throws Exception {
    22         final Example example = new Example();
    23         Thread t1 = new Thread(new Runnable() {
    24             @Override
    25             public void run() {
    26                 example.execute();
    27             }
    28         });
    29         t1.start();
    30 
    31         Thread.sleep(1000);
    32         System.out.println("主线程即将置stop值为true...");
    33         example.setStop(true);
    34         System.out.println("主线程已将stop值为:" + example.getStop());
    35         System.out.println("主线程等待线程1执行完...");
    36 
    37         t1.join();
    38         System.out.println("线程1已执行完毕,整个流程结束...");
    39     }
    40 }

      上面程序的意思是:让线程1先执行然后主(main)线程修改标志看是否能让子线程跳出循环。执行程序后发现程序并没有执行完,而是在等待线程1执行完毕。这就说明主线程修改stop变量并不对线程1可见,所以普通变量是不保证可见性的

      当你把变量stop用volatile修饰时,主线程修改stop变量会立马对线程1可见并终止程序,这就证明volatile变量是具有可见性特性的。下面修改后的结果。

      原子性特性已经说的很清楚了(对任意(包括64位long类型和double类型)单个volatile变量的读/写具有原子性),记着是对单个volatile变量的读或写才具有原子性(如果要进行测试的话,将上面案例的volatile变量修改成long/double类型,测试逻辑一样,只不过将它放在x86的机器上运行。因为在x86的机器上不能保证long类型和double类型的原子性的,具体原因在Java内存模型中的顺序一致性一节有说明)。另外任何复合操作都不能保证原子性,如a++,a = a+1, a = b。特别注意a = b这类,它实际上包含2个操作,它先要去读取b的值,再将b的值写入工作内存,虽然读取b的值以及将b的值写入工作内存这2个操作都是原子性操作,但是合起来就不是原子性操作了

      想要理解透volatile特性有一个很好的方法,就是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。

    三 volatile写-读建立的happens-before关系

      这个详细在happens-before规则中说明。

    四 volatile写-读的内存语义

    • 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存

    • 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量

      以上面VolatileExample程序为例进行简单说明,当主线程对stop进行修改后且子线程尚未对stop进行读时,主线程已经把stop的值刷新到了主内存。其示意图如下:

      当子线程进行读取时,会把本地内存置为无效直接去主内存中读取。(这里的主线程和子线程可以了解为两个普通线程没有父子关系)其示意图如下:

    五 volatile内存语义的实现

      为了实现volatile的内存语义,JMM会分别限制这两种类型的重排序。下图是JMM针对编译器指定的volatile重排序规则表。

    • 当第二个操作为volatile写操作时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序。这个规则确保volatile写之前的所有操作都不会被重排序到volatile写之后;
    • 当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序。这个规则确保volatile读之后的所有操作都不会被重排序到volatile读之前;
    • 当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序。

      为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障(在JMM也有提到过且有说明对应的几种屏障的作用,请仔细阅读)来禁止特定类型的处理器重排序。下面是基于保守策略的 JMM 内存屏障插入策略:

    • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障(禁止前面的写与volatile写重排序)。

    • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(禁止volatile写与后面可能有的读和写重排序)

    • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障(禁止volatile读与后面的读操作重排序)

    • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障(禁止volatile读与后面的写操作重排序)

       其中重点说下StoreLaod屏障,它是确保可见性的关键,因为它会将屏障之前的写缓冲区中的数据全部刷新到主内存中。上述内存屏障插入策略非常保守,但它可以保证在任意处理平台,任意的程序中都能得到正确的volatile语义。下面是保守策略(为什么说保守呢,因为有些在实际的场景是可省略的)下,volatile 写操作 插入内存屏障后生成的指令序列示意图:

      其中StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作对任意处理器可见(把它刷新到主内存)。另外volatile写后面有StoreLoad屏障,此屏障的作用是避免volatile写与后面可能有的读或写操作进行重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)为了保证能正确实现volatile的内存语义,JMM采取了保守策略:在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见模式是:一个写线程写volatile变量,多个度线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里也可看出JMM在实现上的一个特点:首先确保正确性,然后再去追求效率(其实我们工作中编码也是一样)。

      下面是在保守策略下,volatile读插入内存屏障后生产的指令序列示意图:

      上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况忽略不必要的屏障。在JMM基础中就有提到过各个处理器对各个屏障的支持度,其中x86处理器仅会对写-读操作做重排序。

    六 总结

      volatile主要作用是具有可见性和原子性(单个变量),其实现原理就是利用屏障来保障实现。要想彻底掌握就应该多做下相关场景的编码,经典的场景有:状态标记量、volatile方式的double check等。

      以上如有错误之处,欢迎指出,欢迎讨论,谢谢!

       

  • 相关阅读:
    计算机工程-作图(可编辑)
    伪代码规范
    重启随机游走算法(RWR:Random Walk with Restart)
    二分图匹配——通俗易懂
    com.panie 项目开发随笔_爬虫初识(2017.2.7)
    Java获取随机数的3种方法
    我收藏的开源项目
    根据单击当前行的按钮时,获取 该行的其他列 的值
    js中的json对象和字符串之间的转化
    com.panie 项目开发随笔(NoF)_环境搭建(2016.12.29)
  • 原文地址:https://www.cnblogs.com/yuanfy008/p/9335168.html
Copyright © 2011-2022 走看看