• volatile的工作原理


    volatile的特性:

    1. volatile可见性:对一个volatile的读,总可以看到对这个变量最终的写;
    2. volatile原子性:volatile对单个读/写具有原子性(32位Long、Double),但是复合操作除外,例如:i++;
    3. jvm底层采用“内存屏障”来实现volatile语义。

    volatile的内存语义及实现:
      在JMM中,线程之间的通信采用共享内存来实现的。
    volatile内存语义是:

      • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中;
      • 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。

    volatile的底层实现是通过插入内存屏障,但是对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,所以,JMM采用保守策略。如下:

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

    StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中;
    StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
    LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序
    LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序

    java中volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

    在java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。实现方式有所区别:

      1. volatile关键字会禁止指令重排;
      2. 2、synchronized关键字保证同一时刻只允许一条线程操作。 synchronized是万能,他可以同时满足三种特性,这其实也是很多人滥用synchronized的原因。

    volatile实现原理

    1)JMM把内存屏障指令分为下列四类:
    StoreLoad 
    Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer 
    fully flush)。
    Store:数据对其他处理器可见(即:刷新到内存)
    Load:让缓存中的数据失效,重新从主内存加载数据 
    2)JMM针对编译器制定的volatile重排序规则表
    是否能重排序 第二个操作 
    第一个操作 普通读/写 volatile读 volatile写 
    普通读/写     NO 
    volatile读 NO NO NO 
    volatile写   NO NO 
    举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
    从上表我们可以看出:
    当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。 
    当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。 
    * 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。 
    JMM内存屏障插入策略(编译器可以根据具体情况省略不必要的屏障):
    * 在每个volatile写操作的前面插入一个StoreStore屏障。 
    *  对于这样的语句Store1 StoreStore Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见 
    * 在每个volatile写操作的后面插入一个StoreLoad屏障。 
    * 对于这样的语句Store1 StoreLoad Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见 
    * 在每个volatile读操作的后面插入一个LoadLoad屏障。 
    * 对于这样的语句Load1 LoadLoad Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕 
    * 在每个volatile读操作的后面插入一个LoadStore屏障。   
    * 对于这样的语句Load1 LoadStore Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕 
    volatile保证可见性
      volatile修饰的变量写之后将本地内存刷新到主内存,保证了可见性
    volatile保证有序性
      volatile变量读写前后插入内存屏障以避免重排序,保证了有序性
    volatile不保证原子性
      volatile不是锁,与原子性无关
    要我说,由于CPU按照时间片来进行线程调度的,只要是包含多个步骤的操作的执行,天然就是无法保证原子性的。因为这种线程执行,又不像数据库一样可以回滚。如果一个线程要执行的步骤有5步,执行完3步就失去了CPU了,失去后就可能再也不会被调度,这怎么可能保证原子性呢。
    为什么synchronized可以保证原子性 ,因为被synchronized
    修饰的代码片段,在进入之前加了锁,只要他没执行完,其他线程是无法获得锁执行这段代码片段的,就可以保证他内部的代码可以全部被执行。进而保证原子性。

    volatile不保证原子性的例子:

    /** * 创建10个线程,然后分别执行1000次i++操作。目的是程序输出结果10000 * 
    但是,多次执行的结果都小于10000。这其实就是volatile无法满足原子性的原因。*/ 
    public class Test { 
        public volatile int inc = 0; 
    
        public void increase() {
             inc++; 
        } 
    
        public static void main(String[] args) {
            final Test test = new Test(); 
            for (int i = 0; i < 10; i++) {
            new Thread() { 
                public void run() { 
                    for (int j = 0; j < 1000; j++) 
                        test.increase(); 
                    }; 
                }.start(); 
            }
            while (Thread.activeCount() > 1) // 保证前面的线程都执行完 
                Thread.yield(); 
            System.out.println(test.inc); 
        }
     }             

    参考:volatile的实现原理

    参考:volatile的工作原理

  • 相关阅读:
    JVM相关知识
    面试之mysql专题
    Java新特性
    数据结构操作与算法复杂度分析
    IO流
    浅谈Web安全
    面试题2
    需要知道的HTTP 知识
    How JavaScript Work
    webpack 学习笔记
  • 原文地址:https://www.cnblogs.com/heqiyoujing/p/11333208.html
Copyright © 2020-2023  润新知