【死磕Java并发】—–Java内存模型之分析volatile

原文出处:http://cmsblogs.com/chenssy


前篇博客【死磕Java并发】—–深入分析volatile的实现原理 中已经阐述了volatile的特性了:

  1. volatile可见性;对一个volatile的读,总可以看到对这个变量最终的写;
  2. volatile原子性;volatile对单个读/写具有原子性(32位Long、Double),但是复合操作除外,例如i++;
  3. JVM底层采用“内存屏障”来实现volatile语义

下面LZ就通过happens-before原则和volatile的内存语义两个方向介绍volatile。

volatile与happens-before

在这篇博客【死磕Java并发】—–Java内存模型之happend-before中LZ阐述了happens-before是用来判断是否存数据竞争、线程是否安全的主要依据,它保证了多线程环境下的可见性。下面我们就那个经典的例子来分析volatile变量的读写建立的happens-before关系。

public class VolatileTest {

    int i = 0;
    volatile boolean flag = false;

    //Thread A
    public void write(){
        i = 2;              //1
        flag = true;        //2
    }

    //Thread B
    public void read(){
        if(flag){                                   //3
            System.out.println("---i = " + i);      //4
        }
    }
}

依据happens-before原则,就上面程序得到如下关系:

  • 依据happens-before程序顺序原则:1 happens-before 2、3 happens-before 4;
  • 根据happens-before的volatile原则:2 happens-before 3;
  • 根据happens-before的传递性:1 happens-before 4

操作1、操作4存在happens-before关系,那么1一定是对4可见的。可能有同学就会问,操作1、操作2可能会发生重排序啊,会吗?如果看过LZ的博客就会明白,volatile除了保证可见性外,还有就是禁止重排序。所以A线程在写volatile变量之前所有可见的共享变量,在线程B读同一个volatile变量后,将立即变得对线程B可见。

volataile的内存语义及其实现

在JMM中,线程之间的通信采用共享内存来实现的。volatile的内存语义是:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量

所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
那么volatile的内存语义是如何实现的呢?对于一般的变量则会被重排序,而对于volatile则不能,这样会影响其内存语义,所以为了实现volatile的内存语义JMM会限制重排序。其重排序规则如下:

翻译如下:

  1. 如果第一个操作为volatile读,则不管第二个操作是啥,都不能重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前;
  2. 当第二个操作为volatile写是,则不管第一个操作是啥,都不能重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volatile写之后;
  3. 当第一个操作volatile写,第二操作为volatile读时,不能重排序。

volatile的底层实现是通过插入内存屏障,但是对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,所以,JMM采用了保守策略。如下:

  • 在每一个volatile写操作前面插入一个StoreStore屏障
  • 在每一个volatile写操作后面插入一个StoreLoad屏障
  • 在每一个volatile读操作后面插入一个LoadLoad屏障
  • 在每一个volatile读操作后面插入一个LoadStore屏障

StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。

StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。

LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。

LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

下面我们就上面那个VolatileTest例子分析下:

public class VolatileTest {
    int i = 0;
    volatile boolean flag = false;
    public void write(){
        i = 2;
        flag = true;
    }

    public void read(){
        if(flag){
            System.out.println("---i = " + i);
        }
    }
}

上面通过一个例子稍微演示了volatile指令的内存屏障图例。

volatile的内存屏障插入策略非常保守,其实在实际中,只要不改变volatile写-读得内存语义,编译器可以根据具体情况优化,省略不必要的屏障。如下(摘自方腾飞 《Java并发编程的艺术》):

public class VolatileBarrierExample {
    int a = 0;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite(){
        int i = v1;     //volatile读
        int j = v2;     //volatile读
        a = i + j;      //普通读
        v1 = i + 1;     //volatile写
        v2 = j * 2;     //volatile写
    }
}

没有优化的示例图如下:

我们来分析上图有哪些内存屏障指令是多余的

  • 1:这个肯定要保留了
  • 2:禁止下面所有的普通写与上面的volatile读重排序,但是由于存在第二个volatile读,那个普通的读根本无法越过第二个volatile读。所以可以省略。
  • 3:下面已经不存在普通读了,可以省略。
  • 4:保留
  • 5:保留
  • 6:下面跟着一个volatile写,所以可以省略
  • 7:保留
  • 8:保留

所以2、3、6可以省略,其示意图如下:

参考资料

  1. 方腾飞:《Java并发编程的艺术》
赞(7) 打赏

如未加特殊说明,此网站文章均为原创,转载必须注明出处。Java 技术驿站 » 【死磕Java并发】—–Java内存模型之分析volatile
分享到: 更多 (0)

评论 13

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #7

    public class VolatileTest{

    int i = 0;
    volatile boolean flag = false;

    public void write(){
    i = 3;
    flag = true;
    }

    public void read(){
    if(flag)
    System.out.println("i -> " + i);
    }

    public static void main(String[] args){
    while(true){
    VolatileTest test = new VolatileTest();
    new Thread(new Runnable(){
    @Override
    public void run(){
    test.write();
    }
    }).start();

    new Thread(new Runnable(){
    @Override
    public void run(){
    test.read();
    }
    }).start();
    }
    }

    }

    我把volatile去掉也没有什么变化啊

    小黑孩2个月前 (04-17)回复
    • 你这样写是模拟不出来的。你read 哪里加一个 while 循环看看,在来对比加 vaolatile 和 不加 volatile 的区别

      chenssy2个月前 (04-17)回复
  2. #6

    我自己错了!!!

    Sunny___4个月前 (03-01)回复
  3. #5

    在每一个volatile读操作后面插入一个LoadLoad屏障
    在每一个volatile读操作后面插入一个LoadStore屏障

    这里重复了啊

    Sunny___4个月前 (03-01)回复
  4. #4

    public class VolatileTest {
    // int i = 0;
    volatile boolean flag = false;
    public void write(){
    System.out.println("write start");
    // i = 2;
    flag = true;
    System.out.println("write end");
    }

    public void read(){
    // System.out.println(flag);
    if(flag){
    // System.out.println("---i = " + i);
    System.out.println("true");
    } else {
    System.out.println("false");
    }
    }

    public static void main(String[] args) {
    VolatileTest volatileTest = new VolatileTest();
    Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
    volatileTest.write();
    }
    });
    Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
    volatileTest.read();
    }
    });
    t.start();
    t2.start();
    }
    }

    这个代码还是会打印出 write start write end false 打印出write end说明flag已经赋值成true了,按照楼主的文章应该打印出true,但是还是打印了false。请问这是为什么呢?

    龙的传人6个月前 (12-21)回复
    • 程序不是你这么写的程序不对,read() 应该要一个while 循环,因为 read() 仅执行一次,这个时候有可能会比 write() 先执行

      chenssy6个月前 (12-21)回复
      • 就是说他这两个线程因为争夺时间片而不符合hapens-before了呗?能不能join()等方法,保证让Thread1先执行呢?

        小黑孩2个月前 (04-17)回复
  5. #3

    顶一下!!!

    chenssy2年前 (2017-03-25)回复
  6. #2

    整的太复杂了。。。真的需要用这么大的例子来说明么?

    诲孟2年前 (2017-02-24)回复
    • 图太大了而已!这个例子还可以!

      selfImpro2年前 (2017-02-24)回复
  7. #1

    感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/qp121r 欢迎点赞支持!欢迎订阅《java提高篇》https://toutiao.io/subjects/25239

    开发者头条2年前 (2017-02-24)回复

关注【Java 技术驿站】公众号,每天早上 8:10 为你推送一篇技术文章

扫描二维码关注我!


关注【Java 技术驿站】公众号 回复 “VIP”,获取 VIP 地址永久关闭弹出窗口

免费获取资源

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏