前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【java并发编程实战3】解密volatilevolatile的使用场景

【java并发编程实战3】解密volatilevolatile的使用场景

作者头像
yukong
发布2018-10-09 16:02:17
2540
发布2018-10-09 16:02:17
举报
文章被收录于专栏:yukong的小专栏yukong的小专栏

自从jdk1.5以后,volatile可谓发生了翻天覆地的变化,从一个一直被吐槽的关键词,变成一个轻量级的线程通信代名词。

接下来我们将从以下几个方面来分析以下volatile

  • 重排序与as if serial的关系
  • volatile的特点
  • volatile的内存语义
  • volatile的使用场景

重排序与as if serial的关系

重排序值得是编译器与处理器为了优化程序的性能,而对指令序列进行重新排序的。

但是并不是什么情况下都可以重排序的,

  • 数据依赖 a = 1; // 1 b = 2; // 2 在这种情况,1、2不存在数据依赖,是可以重排序的。 a = 1; // 1 b = a; // 2 在这种情况,1、2存在数据依赖,是禁止重排序的。
  • as if serial 简单的理解就是。不管怎么重排序,在单线程情况下程序的执行结果是一致。

根据 as if serial原则,它强调了单线程。那么多线程发生重排序又是怎么样的呢?

请看下面代码

代码语言:javascript
复制
public class VolatileExample1 {

    /**
     * 共享变量 name
     */
    private static String name = "init";


    /**
     * 共享变量 flag
     */
    private  static boolean flag = false;


    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            name = "yukong";            // 1
            flag = true;                // 2
        });
        Thread threadB = new Thread(() -> {
            if (flag) {                 // 3
                System.out.println("flag = " + flag + " name = " +name);  // 4
            };
        });
    }
}

上面代码中,name输出一定是yukong吗,答案是不一定,根据happen-before原则与as if serial

原则,由于 1、2不存在依赖关系,可以重排序,操作3、操作4也不存在数据依赖,也可以重排序。

那么就有可能发生下面的情况

1535967197003.png

上图中,操作1与操作2发生了重排序,程序运行的时候,线程A先将flag更改成true,然后线程B读取flag变量并且判断,由于此时flag已经是true,线程B将继续读取name的值,由于此时线程name的值还没有被线程A写入,那么线程此时输出的name就是初始值,因为在多线程的情况下,重排序存在线程安全问题。

volatile的特点

volatile变量具有以下的特点。

  • 可见性。对于一个volatile变量的读,总是能看到任意线程对这个变量的最后的修改。
  • 有序性。对于存在指令重排序的情况,volatile会禁止部分指令重排序。

这里我先介绍一下volatile关键词的特点,接下来我们将会从它的内存语义来解释,为什么它会具有以上的特点,以及它使用的场景。

volatile的内存语义

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

如果一个场景存在对volatile变量的读写场景,在读线程B读一个volatile变量后,,写线程A在写这个volatile变量前所有的所见的共享变量的值都将会立即变得对读线程B可见。

那么这种内存语义是怎么实现的呢?

其实编译器生产字节码的时候,会在指令序列中插入内存屏障来禁止指令排序。下面就是JMM内存屏障插入的策略。

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

那么这些策略中,插入这些屏障有什么作用呢?我们逐条逐条分析一下。

  • 在每一个volatile写操作前插入一个StoreStore屏障,这条策略保证了volatile写变量与之前的普通变量写不会重排序,即是只有当volatile变量之前的普通变量写完,volatile变量才会写。 这样就保证volatile变量写不会跟它之前的普通变量写重排序
  • 在每一个volatile写操作后插入一个StoreLoad屏障,这条策略保证了volatile写变量与之后的volatile写/读不会重排序,即是只有当volatile变量写完之后,你后面的volatile读写才能操作。 这样就保证volatile变量写不会跟它之后的普通变量读重排序
  • 在每一个volatile读操作后插入一个LoadLoad屏障,这条策略保证了volatile读变量与之后的普通读不会重排序,即只有当前volatile变量读完,之后的普通读才能读。 这样就保证volatile变量读不会跟它之后的普通变量读重排序
  • 在每一个volatile读操作后插入一个LoadStore屏障,这条策略保证了volatile读变量与之后的普通写不会重排序,即只有当前volatile变量读完,之后的普通写才能写。样就保证volatile变量读不会跟它之后的普通变量写重排序

根据这些策略,volatile变量禁止了部分的重排序,这样也是为什么我们会说volatile具有一定的有序的原因。

根据以上分析的volatile的内存语义,大家也就知道了为什么前面我们提到的happen-before原则会有一条

  • volatile的写happen-before与volaile的读。

那么根据volatile的内存语义,我们只需要更改之前的部分代码,只能让它正确的执行。

即把flag定义成一个volatile变量即可。

代码语言:javascript
复制
public class VolatileExample1 {

    /**
     * 共享变量 name
     */
    private static String name = "init";


    /**
     * 共享变量 flag
     */
    private  volatile static boolean flag = false;


    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            name = "yukong";            // 1
            flag = true;                // 2
        });
        Thread threadB = new Thread(() -> {
            if (flag) {                 // 3
                System.out.println("flag = " + flag + " name = " +name);  // 4
            };
        });
    }
}

我们来分析一下

  • 由于 flag是volatile变量 那么在volatile写之前插入一个storestore内存屏障,所以1,2不会发生重排序,即1happen before 2
  • 由于 flag是volatile变量 那么在volatile读之后插入一个loadload内存屏障,所以3,4不会发生重排序,即3happen before 4
  • 根据happen-before原则,volatile写happen before volatile读,即是 2happen before 3。
  • 根据happen-before的传递性,所以1 happen before4。

1535969126569.png

volatile的使用场景

  • 标记变量,也就是上面的flag使用
  • double check 单例模式中

下面我们看看double check的使用

代码语言:javascript
复制
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;
    }
}

至于为何需要这么写请参考:

《Java 中的双重检查(Double-Check)》http://www.iteye.com/topic/652440

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.09.03 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 重排序与as if serial的关系
  • volatile的特点
  • volatile的内存语义
  • volatile的使用场景
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档