• volatile


    前言

    Java 线程安全

    在之前提到:

    通过引入并发来优化执行效率的同时引入了其他问题:

    • 对共享资源的处理无法保证原子性
    • 顺序问题 1. 处理器乱序执行指令在多线程下出问题,2. 多个线程需要顺序执行某个操作
    • 可见性 Java的内存模型(工作内存的出现引入了缓存,缓存导致数据的可见性出现问题)

    volatile 的作用在于保持相关操作的顺序来实现可见性与有序性。

    • 保证可见性: 实现读写有序,在读数据之前一定将最新的当前当前数据在主内存刷新
    • 保证有序性: 是可见性的实现基础,即对读写有序的底层保障,不允许重排序,严格有序。

    先行发生原则中 volatile的描述

    在这里插入图片描述

    保证可见性

    变量修改对其他线程立即可见

    可见性问题

    https://www.pdai.tech/md/java/thread/java-thread-x-key-volatile.html#%E5%AE%9E%E7%8E%B0%E5%8F%AF%E8%A7%81%E6%80%A7

    可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile关键字能有效的解决这个问题,我们看下下面的例子,就可以知道其作用:

    public class VolatileTest {
        int a = 1;
        int b = 2;
    
        public void change(){
            a = 3;
            b = a;
        }
    
        public void print(){
            System.out.println("b="+b+";a="+a);
        }
    
        public static void main(String[] args) {
            while (true){
                final VolatileTest test = new VolatileTest();
                //开启两个线程,一个线程改变值,一个线程输出状态
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        test.change();
                    }
                }).start();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        test.print();
                    }
                }).start();
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    直观上说,这段代码的结果只可能有三种:b=3;a=3 或 b=2;a=1或者b=2;a=3。不过运行上面的代码(可能时间上要长一点),你会发现除了上三种结果之外,还出现了第四种结果:b=3,a=1

    明明无论如何a的变化都应该在b的变化之前,怎么会发生a没有变化,但是b却变化了

    这个过程也可能是重排序以及Java主内存与工作内存之间的可见性问题,我们先来讨论可见性问题。

    在这里插入图片描述
    计算机系统中,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。其模型如下图所示:

    https://tech.meituan.com/2014/09/23/java-memory-reordering.html
    在这里插入图片描述

    在之前我们分析过在并发下就会出现线程协同问题

    并发的发展哲学意义

    volatile 如何实现可见性

    对volatile变量的操作需要遵从先行发生原则,即如果对volatile变量A的写操作先于读操作发生,那么必然该写操作能够被读操作获取到。
    在这里插入图片描述

    这个过程的具体实现还是通过内存屏障,具体细节可查看:

    其实就是通过直接写入内存(而不是通过工作内存)并告诉其他所有读操作该变量已有新值。

    关键字 volatile详解

    保证有序性

    volatile 实现有序性

    同理,volatile 变量规则属于先行发生原则的一部分,不允许被重排序。
    (具体实现通过内存屏障)

    内存屏障

    Volatile通过内存屏障可以禁止指令重排序,内存屏障是一个CPU的指令,它可以保证特定操作的执行顺序。

    内存屏障分为四种:

    StoreStore屏障、StoreLoad屏障、LoadLoad屏障、LoadStore屏障。

    JMM针对编译器制定了Volatile重排序的规则:

    在这里插入图片描述
    LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
    LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

  • 相关阅读:
    [机缘参悟-33]:眼见不一定为实,大多数时候“眼见为虚”
    kotlin根据文件的filePath转化为uri
    硬件管理平台 - 公共项目搭建(Nancy部分)
    mybatis 动态sql和分页
    新版Java面试专题视频教程——准备篇、Redis篇
    vue组件之间8种组件通信方式总结
    2.KDTree相关
    网络安全之应急流程
    【数据结构与算法】杨辉三角,相同字符的截取以及扑克牌
    快速检索并引用你在CSDN上所有的博文笔记
  • 原文地址:https://blog.csdn.net/qq_44587855/article/details/124638470