• Java内存模型-final域的内存语义


    一 引言

      说到final你肯定知道它是Java中的关键字,那么它所在Java中的作用你知道吗?不知道的话,请前往这篇了解下https://www.cnblogs.com/yuanfy008/p/8021673.html

      今天我们来说说final域在JMM中的内存语义。

    二 final域的重排序规则

      开门见山,对于final域,编译器和处理器一定要遵守两个重排序规则(JSR-133才增强了final域):

      1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这个两个操作不能被重排序。

      2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

      下面我们通过案例来说明这两点(假设线程1执行writer(),随后另一个线程执行reader()方法):

    public class FinalExample {
        static volatile boolean flag = true;
        int i = 0;
        final int j;
        static FinalExample obj;
    
        public FinalExample() { // 构造函数
            i = 1;              // 写普通域
            j = 2;              // 写final域
        }
    
        public static void writer() { // 线程1写入
            obj = new FinalExample();
        }
    
        public static void reader() { // 线程2读取
            FinalExample example = obj; // 读对象引用
            System.out.println(example.i); // 读普通域
            System.out.println(example.j); // 读final域
        }
    }

      写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面两个方面:

      1)JMM禁止编译器吧final域的写重排序到构造函数之外。

      2)编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

      所以线程1执行顺序如下图(其中写普通域的顺序无法保证,理论上是存在下面三种情况的,要想验证普通域是否有重排序的结果有点难,因为无法保证线程1把普通域重排序后,线程2能够读取它之前的0值):

      读final域的重排序规则是:在一个线程中,初次读这个对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。其中编译器会在读final域操作的前面插入一个LoadLoad屏障。由于插入了loadLoad屏障,读普通域i的操作是不会重排序到读final域,但是不保证它会重排到读对象引用这个操作的前面。所以线程2的一个执行顺序就能想象到了,这里就不画线程2的执行顺序图了。 

    三 final域为引用类型

      如果finaly域为引用类型,JMM中是怎么处理的呢?对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 

           以上及其以上都要注意:只针对于构造函数方法内。另外要想以上规则确保,还需要一个条件:在构造函数内部,不能让这个被构造对象的引用被其他线程可见,也就是对应引用不能再构造函数中“逸出”。如下案例:

      

    class FinalReferenceEscapeExample {
        final int i;
        static FinalReferenceEscapeExample obj;
    
        public FinalReferenceEscapeExample() {
            i = 1;      // 1
            obj = this; // 2 this引用逸出
        }
    
        public static void writer() { // 线程1
            new FinalReferenceEscapeExample();
        }
    
        public static void reader() { // 线程2
            if (obj != null) {
                System.out.println(obj.i);
            }
        }
    }

      上面程序,第一步写final域与第二步是不保证重排序的。所以当第一步与第二步重排之后,线程1执行完这步(obj = this)后,时间片分给第二个线程执行,那么线程2将会获取final域初始化之前的值,这肯定就违背了程序的初衷。

      

  • 相关阅读:
    创始人透露Twitter新盈利计划:第三方将受益
    试试ScribeFire转发我的其它博客
    2009年12月30日:新网因清除URL转发域名导致DNS解析故障
    VS2008 如果更改源代码管理插件,将关闭活动解决方案或项目
    Linq使用Group By经验总结
    使用C#的BitmapData
    Java与C#开发上的一些差异与转换方法
    成都七中成绩文件导入SQL脚本
    用SQL SERVER对EXCEL数据进行处理
    NULLIF和ISNULL
  • 原文地址:https://www.cnblogs.com/yuanfy008/p/9349275.html
Copyright © 2020-2023  润新知