Volatile内存语义

目录

volatile的可见性

volatile无法保证原子性

volatile禁止重排优化

volatile内存语义的实现


volatile是Java虚拟机提供的轻量级的同步机制。

volatile关键字有如下两个作用

保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。

禁止指令重排序优化。

volatile的可见性

关于volatile的可见性作用,我们必须意识到被volatile修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中

public class VolatileVisibilitySample {
    private boolean initFlag = false;
    static Object object = new Object();

    public void refresh(){
        this.initFlag = true; //普通写操作,(volatile写)
        String threadname = Thread.currentThread().getName();
        System.out.println("线程:"+threadname+":修改共享变量initFlag");
    }

    public void load(){
        String threadname = Thread.currentThread().getName();
        int i = 0;
        while (!initFlag){
            synchronized (object){
                i++;
            }
            //i++;
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i);
    }

    public static void main(String[] args){
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.refresh();
        },"threadA");

        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");

        threadB.start();
        try {
             Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }

}

线程A改变initFlag属性之后,线程B马上感知到

volatile无法保证原子性

/**
 * volatile无法保证原子性
 */
public class VolatileAtomicSample {

    private static volatile int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    counter++; //不是一个原子操作
  
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter);
    }

}

在并发场景下,i变量的任何改变都会立马反应到其他线程中,但是如此存在多条线程同时 调用increase()方法的话,就会出现线程安全问题

毕竟i++;操作并不具备原子性,该操作是 先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一 个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一 个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使 用synchronized修饰,以便保证线程安全

需要注意的是一旦使用synchronized修饰方法 后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就 完全可以省去volatile修饰变量。

volatile禁止重排优化

volatile是如何实 现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)

内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行 顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译 器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器 和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏 障禁止在内存屏障前后的指令执行重排序优化。

Memory Barrier的另外一个作用是强制刷出 各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

总之, volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化

volatile内存语义的实现

重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM 会分别限制这两种类型的重排序类型。 下图是JMM针对编译器制定的volatile重排序规则表。

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

从上图可以看出: 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后。

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

当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来 禁止特定类型的处理器重排序。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数 几乎不可能。为此,JMM采取保守策略。

下面是基于保守策略的JMM内存屏障插入策略。

在每个volatile写操作的前面插入一个StoreStore屏障。

在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。

在每个volatile读操作的后面插入一个LoadStore屏障。

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

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

 

上图中LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。

LoadStore 屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不 改变 volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障

public class VolatileBarrierExample {
    int a;
    volatile int m1 = 1;
    volatile int m2 = 2;

    void readAndWrite() {
        int i = m1;   // 第一个volatile读
        int j = m2;   // 第二个volatile读

        a = i + j;    // 普通写

        m1 = i + 1;   // 第一个volatile写
        m2 = j * 2;   // 第二个 volatile写
    }

}

针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化

注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即 return。此时编 译器可能无法准确断定后面是否会有volatile读或写,为了安全起见, 编译器通常会在这里插 入一个StoreLoad屏障。

上面的优化针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内 存模 型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以X86处理器为 例,图中除最后的StoreLoad屏障外,其他的屏障都会被省略。

前面保守策略下的volatile读和写,在X86处理器平台可以优化成如下图所示。

X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作 做重排序,因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。

在X86中, JMM仅需 在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存 语义。

这意味着在 X86处理器中,volatile写的开销比volatile读的开销会大很多(因为 执行StoreLoad屏障开销会比 较大)。

个人博客:https://www.upheart.top/ 

个人公众号,日常分享一个知识点,每天进步一点点,面试不慌

原创文章 22 获赞 217 访问量 16万+

猜你喜欢

转载自blog.csdn.net/qq_35508033/article/details/106130749
今日推荐