Java内存模型(三)-Volatile

来源:互联网 发布:淘宝花呗分期退货 编辑:程序博客网 时间:2024/06/09 14:34

本文转自:http://www.infoq.com/cn/articles/java-memory-model-4#anch96578

1.volatile特性

    当我们声明共享变量的类型为volatile之后,对这个变量的读写将会很特别。理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步。下面我们通过具体的示例来说明,请看下面的示例代码:

class VolatileFeaturesExample {    volatile long vl = 0L;  //使用volatile声明64位的long型变量    public void set(long l) {        vl = l;   //单个volatile变量的写    }    public void getAndIncrement () {        vl++;    //复合(多个)volatile变量的读/写    }    public long get() {        return vl;   //单个volatile变量的读    }}假设有多个线程分别调用上面程序的三个方法,这个程序在语意上和下面程序等价:class VolatileFeaturesExample {    long vl = 0L;               // 64位的long型普通变量    public synchronized void set(long l) {     //对单个的普通 变量的写用同一个监视器同步        vl = l;    }    public void getAndIncrement () { //普通方法调用        long temp = get();           //调用已同步的读方法        temp += 1L;                  //普通写操作        set(temp);                   //调用已同步的写方法    }    public synchronized long get() {     //对单个的普通变量的读用同一个监视器同步        return vl;    }}

    如上面实例程序所示,对一个volatile变量的读写操作,与对一个普通变量的读/写使用同一个监视器来同步,他们之间的执行效果相同。

    监视器的happens-befor规则保证释放监视器和获取监视器两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

    监视器锁的语义决定了临界区代码的执行具有原子性。这意味着即使是64位的long型或者double型变量,只要他是volatile变量,对该变量的读写就将具有原子性。如果是对多个volatile读写或者类似于volatile++这种复合操作,这些操作整体上没有原子性。

    简而言之,volatile变量自身具有以下的特性:

  • 可见性,对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量的最后写入。
  • 原子性,对任意单个volatile变量的读写具有原子性,但类似于volatile++这种复合操作不具有原子性。

2.volatile写-读建立的happens-before关系

    上面讲的是volatile变量自身的特性,对程序员来说,volatile对线程的内存可见性的影响比volatile自身的特性更为重要,也更需要我们去关注。

    从JSR-133开始,volatile变量的写-读可以实现线程之间的通信。

    从内存语义的角度来说,volatile与监视器锁有相同的效果:volatile写和监视器的释放有相同的内存语义。Volatile读与监视器的获取有相同的语义。

    请看下面使用volatile变量的示例代码:

class VolatileExample {    int a = 0;    volatile boolean flag = false;    public void writer() {        a = 1;                   //1        flag = true;               //2    }    public void reader() {        if (flag) {                //3            int i =  a;           //4            ……        }    }}
    假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-bofore规则,这个过程建立的happens-before规则可以分为两类:
  • 根据程序依次规则:hb(1,2),hb(3,4)

  • 根据volatile规则,hb(2,3)

  • 根据happens-before的传递性规则,hb(1,4)

     Ps:happens-before规则可以简单理解为,如果hb(a,b),a及之前的写操作在另一个线程t中进行b操作时对t是可见的。

    上述happens before 关系的图形化表现形式如下:

   

    在上图中,每一个箭头链接的两个节点,代表了一个hb关系,黑色箭头表示了程序顺序规则;橙色箭头表示volatile规则;蓝色箭头表示组合这些规则后提供的hb保证。

这里A线程写一个volatile变量后,B线程读同一个volatile变量。A在写v变量之前的所有可见共享变量,在B读取同一个V之后,将立即变得对B可见。

3.volatile写-读的内存语义

    Volatile写的内存语义如下:

    当写一个volatile变量的时候,JMM会把线程对应的本地存储中的共享变量刷新到主内存中去。

    Volatile写的内存语义如下:

    当读取一个volatile变量时,JMM会把线程对应的本地存储设置为无效,线程接下来将从主内存中读取共享变量

    如果我们把volatile读和写综合起来看的话,在线程B读取到一个volatile变量之后,写线程A在写v之前所有可见的共享变量的值都将变的立即对B可见。

    下面对volatile写和volatile读的内存语义做个总结:

  •     线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
  •     线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  •     线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

4.volatile内存语义的实现

    下面,让我们来看JMM是如何实现volatile变量写/读的内存语义。

    前文我们提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下面时JMM针对编译器制定的重排序规则表。

     

    举例来说,比如第三行最后一个格的意思是,在程序顺序中,如果第一个操作是普通的变量读写,而第二个操作是volatile写,则编译器不能重排序这两个规则。

从上表我们可以看出:

  •  当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排到volatile之后。
  •  当第一个操作是volatile读的时候,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  •  当第一个操所是volatile写,第二个操作是volatile读的时候,不能重排序。  

    为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM采取保守策略。下面是基于保守策略的JMM内存插入屏障策略:

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

    上述内存屏障的插入策略非常保守,它可以保证在任意的处理器平台,任意的程序中都能得到正确volatile内存语义。

    下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:

上图中的StoreStore屏障可以保证在Volatile写之前,其前面所有的普通操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前已经刷

上图中的StoreStore屏障可以保证在Volatile写之前,其前面所有的普通操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前已经刷新到主内存中去了

   

    上图中的StoreStore屏障可以保证在Volatile写之前,其前面所有的普通操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前已经刷新到主内存中去了。

    这里比较有意思的是volatile写后面的StoreLoad屏障,这个屏障的作用是避免v写与后面可能有的v写/读操作重排序。因为编译器常常无法准确的预测在一个v写的后面,是否需要插入一个StoreLoad屏障(比如,一个v写之后直接return)。为了保证能正确实现v的内存语义,JMM在这里采取了保守策略:在每个v写之后或者在每个v读前插入一个StoreLoad屏障。从整体的执行执行效率考虑,JMM选择在每个v写后面加入StoreLoad屏障。因为v读/写语义常见的使用模式是:一个写线程写v变量,多个读线程读取同一个v变量。当读线程的数量大大超过写线程的时候,选择在v之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里我们看到JMM在实现上的一个特点:首先确保正确性,然后追求效率。


    下面是在保守状态下,v读操作插入内存屏障后生成的指令序列示意图:

   

    上图中的LoadLoad屏障用来禁止上面的v读与下面的普通读重排序。LoadStore用来禁止下面的普通写与v读重排序。

原创粉丝点击