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

自从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原则,它强调了单线程。那么多线程发生重排序又是怎么样的呢?

请看下面代码

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变量即可。

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的使用

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Linux驱动

C语言异常处理之 setjmp()和longjmp()

异常处理之除0情况 相信大家处理除0时,都会通过函数,然后判断除数是否为0,代码如下所示: double divide(doublea,double b) { ...

3364
来自专栏杨建荣的学习笔记

关于正则表达式第一篇(r3笔记第29天)

正则表达式在编程语言中,数据库中,linux中都有着广泛的应用,一说起正则表达式就有些高深晦涩的味道,正则表达式精炼而重要,在Linux中有着举足轻重的作用,也...

3184
来自专栏博岩Java大讲堂

Java虚拟机--你的对象有多大如何计算对象大小

4225
来自专栏技术小黑屋

Java中的堆和栈的区别

当一个人开始学习Java或者其他编程语言的时候,会接触到堆和栈,由于一开始没有明确清晰的说明解释,很多人会产生很多疑问,什么是堆,什么是栈,堆和栈有什么区别?更...

1203
来自专栏PHP实战技术

PHP垃圾回收机制

PHP垃圾回收机制 1、每一个变量定义时都保存在一个叫zval的容器里面,这里面包含了数量的类型和和值,还包含了一个refcount(理解为存在几个变量个数)和...

3234
来自专栏Java后端技术栈

如何在你的项目中使用JSR 303 - Bean Validation进行数值校验?

JSR-303 是 Java EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是hibernate Validator。

1874
来自专栏Java技术栈

IntegerCache的妙用和陷阱!

考虑下面的小程序,你认为会输出为什么结果? public class Test { public static void main(String[...

3555
来自专栏PHP实战技术

PHP垃圾回收机制

1、每一个变量定义时都保存在一个叫zval的容器里面,这里面包含了数量的类型和和值,还包含了一个refcount(理解为存在几个变量个数)和is_ref(理解为...

4015
来自专栏java一日一条

Java 多线程并发编程之 Synchronized 关键字

现有一成员变量 Test,当线程 A 调用 Test 的 synchronized 方法,线程 A 获得 Test 的同步锁,同时,线程 B 也去调用 Test...

2042
来自专栏海天一树

小朋友学Python(19):异常

一、什么是异常 异常即是一个事件,该事件会在程序执行过程中发生,影响了程序的正常执行。 一般情况下,在Python无法正常处理程序时就会发生一个异常。 异常是P...

3389

扫码关注云+社区

领取腾讯云代金券