• 多线程原理篇(3)-volatile关键字


    8.1 几个基本概念

    在介绍volatile之前,我们先回顾及介绍几个基本的概念。

    8.1.1 内存可见性

    在Java内存模型那一章我们介绍了JMM有一个主内存,每个线程有自己私有的工作内存,工作内存中保存了一些变量在主内存的拷贝。

    内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。

    8.1.2 重排序

    为优化程序性能,对原有的指令执行顺序进行优化重新排序(Java代码与实际运行的指令顺序不一致)。重排序可能发生在多个阶段,比如编译重排序、CPU重排序等。

    8.1.3 happens-before规则

    是一个给程序员使用的规则,只要程序员在写代码的时候遵循happens-before规则,JVM就能保证指令在多线程之间的顺序性符合程序员的预期

    8.2 volatile的内存语义

    8.2.1 内存可见性

    在Java中,volatile关键字有特殊的内存语义。volatile主要有以下两个功能:

    • 保证变量的内存可见性
    • 禁止volatile变量与普通变量重排序(JSR133提出,Java 5 开始才有这个“增强的volatile内存语义”)

    以一段代码分析开始:

    public class VolatileDemo {
        boolean flag = true;
    
        public static void  main(String[] args){
            VolatileDemo volatileDemo = new VolatileDemo();
            new Thread(() -> {
                try {
                    volatileDemo.write();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            new Thread(() -> volatileDemo.read()).start();
        }
    
        public void write() throws InterruptedException {
            Thread.sleep(100);
            flag = false;//1
        }
    
        public void read(){
            int i = 0;
            while (flag){//2
                i = i + 1;
            }
        }
    }
    

      2优先执行了,程序一直无法退出。1最后休眠时间到了,重新获取CPU时间片,也执行了,但是程序一直无法退出。因为read线程中的flag缓存值为ture。我们加volatile关键字,程序就可以退出了。volatile提供了内存可见性。

      所谓内存可见性,指的是当一个线程对volatile修饰的变量进行写操作(比如step 1)时,JMM会立即把该线程对应的本地内存中的共享变量的值刷新到主内存;当一个线程对volatile修饰的变量进行读操作(比如step 2)时,JMM会把立即该线程对应的本地内存置为无效,从主内存中读取共享变量的值。

    在这一点上,volatile与锁具有相同的内存效果,volatile变量的写和锁的释放具有相同的内存语义,volatile变量的读和锁的获取具有相同的内存语义。

    8.2.1 禁止(严格限制)重排序

    为了提供一种比锁更轻量级的线程间的通信机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序。

    什么是内存屏障?硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作用:

    1. 阻止屏障两侧的指令重排序;
    2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。

    编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:

    • 在每个volatile写操作前插入一个StoreStore屏障;
    • 在每个volatile写操作后插入一个StoreLoad屏障;
    • 在每个volatile读操作后插入一个LoadLoad屏障;
    • 在每个volatile读操作后再插入一个LoadStore屏障。

    再逐个解释一下这几个屏障。注:下述Load代表读操作,Store代表写操作

    LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,这个屏障会吧Store1强制刷新到内存,保证Store1的写入操作对其它处理器可见。
    LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

    对于连续多个volatile变量读或者连续多个volatile变量写,编译器做了一定的优化来提高性能,比如:

    第一个volatile读;

    LoadLoad屏障;

    第二个volatile读;

    LoadStore屏障

    再介绍一下volatile与普通变量的重排序规则:

    1. 如果第一个操作是volatile读,那无论第二个操作是什么,都不能重排序;

    2. 如果第二个操作是volatile写,那无论第一个操作是什么,都不能重排序;

    3. 如果第一个操作是volatile写,第二个操作是volatile读,那不能重排序。

    举个例子,我们在案例中step 1,是普通变量的写,step 2是volatile变量的写,那符合第2个规则,这两个steps不能重排序。而step 3是volatile变量读,step 4是普通变量读,符合第1个规则,同样不能重排序。

    但如果是下列情况:第一个操作是普通变量读,第二个操作是volatile变量读,那是可以重排序的:

    8.3 volatile的用途

    从volatile的内存语义上来看,volatile可以保证内存可见性且禁止重排序。

    在保证内存可见性这一点上,volatile有着与锁相同的内存语义,所以可以作为一个“轻量级”的锁来使用。但由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁可以保证整个临界区代码的执行具有原子性。所以在功能上,锁比volatile更强大;在性能上,volatile更有优势。

    在禁止重排序这一点上,volatile也是非常有用的。比如我们熟悉的单例模式,其中有一种实现方式是“双重锁检查”,比如这样的代码:

    public class Singleton {
    
        private static Singleton instance; // 不使用volatile关键字
    
        // 双重锁检验
        public static Singleton getInstance() {
            if (instance == null) { // 第7行
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton(); // 第10行
                    }
                }
            }
            return instance;
        }
    }
    

      

    如果这里的变量声明不使用volatile关键字,是可能会发生错误的。它可能会被重排序:

    instance = new Singleton(); // 第10行
    
    // 可以分解为以下三个步骤
    1 memory=allocate();// 分配内存 相当于c的malloc
    2 ctorInstanc(memory) //初始化对象
    3 s=memory //设置s指向刚分配的地址
    
    // 上述三个步骤可能会被重排序为 1-3-2,也就是:
    1 memory=allocate();// 分配内存 相当于c的malloc
    3 s=memory //设置s指向刚分配的地址
    2 ctorInstanc(memory) //初始化对象
    

      

    而一旦假设发生了这样的重排序,比如线程A在第10行执行了步骤1和步骤3,但是步骤2还没有执行完。这个时候另一个线程B执行到了第7行,它会判定instance不为空,然后直接返回了一个未初始化完成的instance!

    所以JSR-133对volatile做了增强后,volatile的禁止重排序功能还是非常有用的。

    总结:1.volatile关键字比较重要,有两个功能:实现内存的可见性和严格限制重排序。重排序有4个内存屏障来实现,store和load组成4个屏障指令,不允许重排序的规则:(1)第一个操作是volatile读的话,第二个操作不论是什么不能重排序;(2)第一个操作是volatile写的话,第二个操作是volatile写/读的都不能重排序;(3)第二个操作是volatile写的话,第一个操作不论什么都不能重排序。

             2.与锁相比,volatile更加轻量级,只对volatile变量的读写具有原子性,锁保证同步块具有原子性,性能上volatile更有优势。

    原文地址:http://redspider.group:4000/article/02/8.html

  • 相关阅读:
    字符串替换
    字符串查找
    字符串比较
    字节与字符串相互转换
    1365. How Many Numbers Are Smaller Than the Current Number
    1486. XOR Operation in an Array
    1431. Kids With the Greatest Number of Candies
    1470. Shuffle the Array
    1480. Running Sum of 1d Array
    【STM32H7教程】第56章 STM32H7的DMA2D应用之刷色块,位图和Alpha混合
  • 原文地址:https://www.cnblogs.com/knsbyoo/p/14029422.html
Copyright © 2020-2023  润新知