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

还记得上一篇文章当中提到的内存屏障(Memory Fence)吗?其实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() {
        // 此处读取数据ready,会增加一个读屏障,保证后面的ready和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。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容