zoukankan      html  css  js  c++  java
  • Java并发(六):volatile的实现原理

    synchronized是一个重量级的锁,volatile通常被比喻成轻量级的synchronized

    volatile是一个变量修饰符,只能用来修饰变量。

    volatile写:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。

    volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

    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修饰的代码片段,在进入之前加了锁,只要他没执行完,其他线程是无法获得锁执行这段代码片段的,就可以保证他内部的代码可以全部被执行。进而保证原子性。

    (摘自http://www.hollischuang.com/archives/2673)

    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写-读得内存语义,编译器可以根据具体情况优化,省略不必要的屏障。参考:【死磕Java并发】—–Java内存模型之分析volatile

    参考资料

    深入理解Java中的volatile关键字

    Java并发(一):Java内存模型干货总结

    【死磕Java并发】—–深入分析volatile的实现原理

    再有人问你volatile是什么,把这篇文章也发给他。

  • 相关阅读:
    Validation failed for one or more entities. See 'EntityValidationErrors' property for more details
    Visual Studio断点调试, 无法监视变量, 提示无法计算表达式
    ASP.NET MVC中MaxLength特性设置无效
    项目从.NET 4.5迁移到.NET 4.0遇到的问题
    发布网站时应该把debug设置false
    什么时候用var关键字
    扩展方法略好于帮助方法
    在基类构造器中调用虚方法需谨慎
    ASP.NET MVC中商品模块小样
    ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现
  • 原文地址:https://www.cnblogs.com/hexinwei1/p/9895521.html
Copyright © 2011-2022 走看看