java并发编程(十六)带你了解volatile原理

  • 对于volatile修饰的变量:

    • 在该变量的写指令后,会加入写屏障
    • 在该变量的读指令前,会加入读屏障

上面先放个结论,后面我们逐步的看它是什么意思。

我们看下有如下的代码,主要是为了理解写屏障和读屏障是如何添加,且填在的位置在何处:

public class VolatileTest {

    /**
     * 定义一个volatile修饰的共享变量
     */
    volatile static boolean flag = false;

    /**
     * 定义全局变量num
     */
    static int num = 0;

    public static void test1() {
        num = 2;
        // 此处修改数据ready,会增加一个写屏障,从而num、ready在修改数据后,都会添加到主存当中
        flag = true;
    }

    public static void test2() {
        // 此处读取数据flag,会增加一个读屏障,保证后面的flag和num都会从主存当中获取数据
        if (flag) {
            System.out.println(num);
        }
    }

    public static void main(String[] args) {

        new Thread(() -> {
            test1();
        }, "t1").start();

        new Thread(() -> {
            test2();
        }, "t2").start();
    }
}

如上所示,有volatile修饰的变量flag,假设上述代码t1先执行,t2后执行,会有如下过程:

  • t1执行test1方法,此时将num赋值称为2,num此时可能没有推送到主存当中。之后又执行了对flag赋值的操作,因为flag是volatile修饰的,所以一定会将flag更新到主存,同时将num也会更新到主存。

  • t2执行test2方法时,首先会读取flag的值,由于flag是有volatile修饰,此时会从主存拉取flag的值,同时num也会从主存获取。

一、可见性如何保证?

前文说到,写屏障对于共享变量的所有修改,在写屏障前的所有共享变量,都需要同步到主内存当中。

读屏障对于共享变量的所有修改,在读屏障后的所有共享变量,都需要同从主存当中获取。

在文章开始的例子当中已经阐述了流程:

  • 在修改flag的值时,所依靠的是写屏障,会在flag被修改后的位置添加一个写屏障,在写屏障之前的的num、和flag修改后的值都会同步到主存当中。

  • 在读取flag的值时,所依靠的是读屏障,在flag读取之前增加一份读屏障,在读屏障后读取的flag和num都会从主存当中获取。

二、有序性如何保证?

  • 写屏障保证在发生指令重排序时,不会将写屏障之前的代码放在写屏障之后。

  • 读屏障会确保指令重排序时,不会将读屏障后的代码放在读屏障之前。

假设在volatile关键字之前有多个变量被修改的语句,那么volatile是不能保证其执行的顺序,能保证的仅仅是在写屏障前的所有代码都执行完毕,并且写屏障前的修改对于读屏障后代码一定是可见的。

假如读取在写屏障之前,那么则不能保证了。

另外需要注意的是,有序性只保证在当前线程内的代码不被重排序。

三、happens-before原则

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,可以说它是可见性与有序性的一套规则总结。

JMM(java memory model,java内存模型)在以下的情况可以保证,线程对共享变量的写,对于其他线程是读可见的,最常见的有以下两种:

  • 使用synchronized关键字

    前面的文章提到过,当使用重量级锁时,对于共享变量的修改是要同步到主存的。

  • 使用volatile修饰的共享变量

还有以下场景(更多的不在下面举例了):

  • 当线程修改共享变量的值,其结束后,其他线程对于修改后的值是可见的。

  • 线程start()之前,对于变量修改后的值,对其是可见的。

  • 线程t1修改变量的值,随后对正在读取该变量的t2进行打断,此时t1打断线程t2,则t2对于修改后的变量读可见。

四、Double-Checked Locking

相信同学们都学习过单例模式,应该都知道其有很多种实现方式,其中有一种就是double-checked locking(双重检查锁)的方式,如下所示:


 

public class Singleton {

    /**
     * volatile 解决指令重排序导致的问题
     */
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    private Singleton() {
    }
}

通过我们的尝试知道DCL一定要加上volatile关键字去修饰实例变量instance,那么是为什么呢?

我们先假设没有加volatile关键字的情况,这种情况下砸多线程情况下是会存在问题的。

如下所示,是在没有添加volatile关键字时的字节码文件:

public class com.cloud.bssp.designpatterns.singleton.lazy.dcl.Singleton {
  public static com.cloud.bssp.designpatterns.singleton.lazy.dcl.Singleton getInstance();
    Code:
       0: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
       3: ifnonnull     37
       6: ldc           #2                  // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      14: ifnonnull     27
      17: new           #2                  // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
      20: dup
      21: invokespecial #3                  // Method "<init>":()V
      24: putstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      27: aload_0
      28: monitorexit
      29: goto          37
      32: astore_1
      33: aload_0
      34: monitorexit
      35: aload_1
      36: athrow
      37: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      40: areturn
    Exception table:
       from    to  target type
          11    29    32   any
          32    35    32   any
}

我们需要了解的是,jvm创建一个完整的对象实例需要两个步骤:

  • 实例化一个对象,即new 出来的对象,此时是一个默认的空对象,其属性等并没有赋值,只是创建了引用,我们可以认为此时是一个半初始化对象。

  • 初始化步骤,此时需要去调用对象的构造方法,完成属性的赋值等操作,只有经过此步骤才是一个完成的对象。

对应到上面的字节码文件,分别是以下的代码:

  • 17:创建一个引用,将引用入栈
  • 20:复制地址引用,用于后面使用
  • 21:通过前面复制的地址引用,调用对象的构造方法
  • 24:将引用赋值到静态变量instance上

相信同学们应该能够对应的上的。

在jvm中呢,如果完全按照上面的步骤执行则不会有问题,但是jvm会优化为先执行24步骤,再执行21步骤,那么结果可想而知,此时静态变量是一个半初始化的对象。

当另外的线程来执行getInstance方法时,获取静态实例对象instance,即字节码文件的第0行,此行代码是在锁synchronized(管程monitorenter)之外,谁来都可以执行,那么获取到了就是半初始对象,不是null,那么一定是有问题的。

通过我们前面的学习,就可以用volatile来解决DCL的这个问题:

这个volatile关键字在字节码是体现不出来的,但是手动标记一下它的位置,只保留主要位置:

      0: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
       --------------------- 此处加入读屏障 --------------------
       3: ifnonnull     37
       6: ldc           #2                  // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      14: ifnonnull     27
      17: new           #2                  // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
      20: dup
      21: invokespecial #3                  // Method "<init>":()V
      24: putstatic     #1                  // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
      --------------------- 此处加入写屏障 --------------------
      27: aload_0
      28: monitorexit

但是根据我们前面学习的,写屏障似乎并不能保证21和24的顺序不变啊,因为都是在写屏障之前,它只能保证写屏障之前的代码不会被放到写屏障后。那么它是如何解决的呢?

其实在更加底层volatile转成汇编语言,是在该代码上增加了lock前缀,此时会将其之前的代码锁住,直到执行到这个lock,此时前面的代码都一定执行完了。

从根本说volatile的实现是是一条CPU原语 lock addl。

太过底层就不多赘述了,毕竟我也没学到位呢!!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值