volatile与内存屏障

1、概述

通过Java高并发--volatile的使用和原理这篇文章,我们了解到volatile关键字其实就两个作用:(1)保证变量的可见性。(2)防止指令重排。而针对变量的可见性我们知道是读volatile变量的时候直接从内存中读,而写volatile变量的时候直接写入内存。那么重排序呢?

2重排序

2.1定义

所谓重排序是指编译器和处理器为了提高程序的执行效率,在不违背happens-before规则的前提下针对程序的执行顺序进行重新排序的处理。

2.2重排序规则

(1)如果第一个操作为volatile读的时候,不管第二个操作是啥,都不能重排序。这条规则确保了volatile读之后的操作不会被编译器重排序到volatile读之前。

(2)如果第二个操作为volatile写的时候,不管第一个操作是啥,都不能重排序。这条规则确保了volatile写治安的操作不会被编译器重排序带volatile写之后。

(3)如果第一个操作为volatile写,第二个操作为volatile读的时候,不能重排序。

那么针对这一些排序规则,volatile底层到底是怎么实现的呢?这就要靠我们的内存屏障来进行保证了。

3内存屏障

3.1内存屏障规则

(1)在每一个volatile写操作前面插入一个StoreStore屏障。这确保了在进行volatile写之前前面的所有普通的写操作都已经刷新到了内存。

(2)在每一个volatile写操作后面插入一个StoreLoad屏障。这样可以避免volatile写操作与后面可能存在的volatile读写操作发生重排序。

(3)在每一个volatile读操作后面插入一个LoadLoad屏障。这样可以避免volatile读操作和后面普通的读操作进行重排序。

(4)在每一个volatile读操作后面插入一个LoadStore屏障。这样可以避免volatile读操作和后面普通的写操作进行重排序。

3.2实例

针对上面我们已经了解了内存屏障的规则,那么现在我们来看看具体的例子。

public class VolatileTest {
    int i = 0;
    volatile boolean flag = false;
    public void write(){
        i = 2;
        flag = true;
    }

    public void read(){
        if(flag){
            System.out.println("---i = " + i); 
        }
    }}

针对上面的例子,内存屏障的插入情况如下:

volatile与内存屏障_第1张图片

上面通过一个例子稍微演示了volatile指令的内存屏障图例。

volatile的内存屏障插入策略非常保守,其实在实际中,只要不改变volatile写-读得内存语义,编译器可以根据具体情况优化,省略不必要的屏障。如下(摘自方腾飞 《Java并发编程的艺术》)

public class VolatileBarrierExample {
    int a = 0;
    volatile int v1 = 1;
    volatile int v2 = 2;
    
    void readAndWrite(){
        int i = v1;     //volatile读
        int j = v2;     //volatile读
        a = i + j;      //普通读
        v1 = i + 1;     //volatile写
        v2 = j * 2;     //volatile写
    }}

没有优化的示例图如下:

volatile与内存屏障_第2张图片

我们来分析上图有哪些内存屏障指令是多余的。

1:这个肯定要保留了

2:禁止下面所有的普通写与上面的volatile读重排序,但是由于存在第二个volatile读,那个普通的读根本无法越过第二个volatile读。所以可以省略。

3:下面已经不存在普通读了,可以省略。

4:保留

5:保留

6:下面跟着一个volatile写,所以可以省略

7:保留

8:保留

所以2、3、6可以省略,其示意图如下:

volatile与内存屏障_第3张图片

4参考资料

方腾飞:《Java并发编程的艺术》

你可能感兴趣的:(JVM)