• Java并发拾遗(三)——volatile


    一、volatile关键字的语义

    当用volatile关键字修饰一个变量时,这个变量就会有一点特殊:

    1. 可见性:volatile能够保证其所修饰变量的可见性,即一个线程对volatile变量的写,对后续的读是立即可见的

    2. 原子性:volatile是无法保证对变量复合操作的原子性的(如volatile无法保证自增++操作的原子性)

    3. 禁止重排序:对volatile修饰的变量的写操作对其他线程是立即可见的,那么这次写操作刷新到主内存时,必须保证刷新之前这个线程所执行的代码具有顺序一致性,不然就会出错了。所以相当于是以volatile变量的写操作为屏障,写操作之前的指令,绝对不能重排序到写操作之后。写操作之后的指令也绝对不能重排序到写操作之前。这就是所谓的禁止重排序吧。

    从实际效果上来讲,当一个变量用了volatile来修饰之后,针对这个变量来说,各个线程的本地内存相当于失效了。对volatile变量的写,不再缓存在线程的本地内存中,而是立即刷到主内存中,对volatile变量的读,不再从本地缓存中读了,而是到主内存中去读。(此处说的是volatile的作用效果,不是实际运行过程,实际运行时线程与主内存的数据交互必须经过缓存)

    二、volatile与happens-before

    再以前文提到的栗子为例:

        private class Test {
            int a = 0;
            boolean flag = false;
    
            public void write() {
                a = 1;             // 1
                flag = true;       // 2
            }
    
            public int read() {
                if(flag) {         // 3
                    int i = a;     // 4
                }
    
                // **
            }
        }
    

      上篇文章说道,在上面的程序中,由于在write与read方法中均存在重排序,导致了变量i赋值的不确定。然而,当变量flag用volatile进行修饰后,就变得有点不同了:

         假设线程A执行write方法之后,线程B再开始执行read。当没有volatile修饰flag时,线程A对flag的赋值未必对B是可见的,结果仍然是不确定的。但当用volatile对flag进行修饰之后,线程A对flag的赋值的结果对线程B就立即可见了。这块说的是线程A先执行,对于A与B同时执行时,属于并发的调度问题,暂不考虑。

    此外,由volatile提供的happens-before保证:对volatile变量的写 happens-before 后续对这个变量的读。在线程A执行write方法之后,线程B再开始执行read的假设下,就有:

    1. 根据程序次序规则,1 happens before 2; 3 happens before 4。

    2. 根据 volatile 规则,2 happens before 3。

    3. 根据 happens before 的传递性规则,1 happens before 4。 

    那么,可见,通过volatile提供的可见性保证和happens-before规则,解决了程序中的可见性问题和重排序问题。(这里再重申一下write方法的重排序问题,当flag用volatile修饰时,随flag的写操作是要立即刷回主内存的,这时候要是a还没完成赋值,就必然错乱了,volatile必然不会允许这种事情的放生,所以即使从外部来看,write方法内也必须是先赋值a再赋值flag,相当于是volatile禁止了write方法的重排序。)

     三、volatile语义的实现

    从上面所述,volatile主要做了两点保证:

    1. 可见性:写操作之后需要对其他线程理解可见

    2. 禁止重排序:以写操作为屏障,禁止屏障两侧的重排序刺穿屏障

    可知,对于可见性的保证,线程需要能够将缓存值立即刷回主内存的能力,而禁止重排序则需要通过在编译出来的字节码中的特定位置插入内存屏障,来禁止处理器级别的重排序。无论哪种,都必然需要处理器指令级别的支持了。

    在怼volatile变量的写操作后,通过处理器级别的内存屏障指令,将写入的值立即写回到主内存中。在对volatile的变量进行读操作前,也是通过内存屏障指令将线程本地内存的对应缓存变量置为失效,并从主内存中重新读取变量的最新值。同时,由于内存屏障指令(如Intel处理器的Lock指令)本身就具有禁止重排序的功能,因此即实现了volatile的语义。

  • 相关阅读:
    qemu+chroot构建arm aarch64虚拟机
    <转>Linux环境下段错误的产生原因及调试方法小结
    <转>PCA的数学原理
    博客分类整理
    detectron2 配置记录
    如何读取部分的预训练模型
    重新配置语义分割实验环境遇到的坑
    pytorch 调整tensor的维度位置
    seg代码配置的踩坑记录
    Alienware R8外星人台式机安装双系统(WIN10+Ubuntu)的总结
  • 原文地址:https://www.cnblogs.com/dosmile/p/6726034.html
Copyright © 2020-2023  润新知