java内存模型之-final域

1.1final域的重排序规则

对于final域,编译器和处理器要遵守两个重排序规则。
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用
变量,这两个操作之间不能重排序。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能
重排序。

 1.1.1写final域重排序规则

  写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含 下面2个方面。
  1)JMM禁止编译器把final域的写重排序到构造函数之外。
  2)编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障 禁止处理器把final域的写重排序到构造函数之外。 

  写普通域的操作被编译器重排序到了构造函数之外,读线程B错误地读取了 普通变量i初始化之前的值。而写final域的操作,被写final域的重排序规则“限定”在了构造函数 之内,读线程B正确地读取了final变量初始化之后的值。
  写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被 正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程B“看到”对象引用obj时, 很可能obj对象还没有构造完成(对普通域i的写操作被重排序到构造函数外,此时初始值1还 没有写入普通域i)。

 

  1.1.2读final域重排序规则  

  读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final 域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final 域操作的前面插入一个LoadLoad屏障。

1.2 final 域为引用类型

  对于引用类型,写final域的重 排序规则对编译器和处理器增加了如下约束:在构造函数内对一个final引用的对象的成员域

的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。下面用样例程序解释一下:

public class FinalReferenceExample {
    final int [] intArray;          //final 是引用
    static FinalReferenceExample obj;
    public FinalReferenceExample (){  // 构造函数
        intArray = new int[1];        //1
        intArray[0] =1;               //2
    }
    public static  void writeOwo(){   //写线程A执行
        obj = new FinalReferenceExample();           // 3
    }
    public static  void writeTwo(){   //写线程A执行
        obj.intArray[0] = 2;           // 3
    }
    public static void reader(){      //写线程B执行
        if(obj != null){              // 4
            int temp1 = obj.intArray[0];
        }
    }
}

  1是对final域的写入,2是对这个final域引用的对象的成员域的写入,3是把被 构造的对象的引用赋值给某个引用变量。这里除了前面提到的1不能和3重排序外,2和3也不能重排序。

1.3 final引用不能从构造函数内溢出

public class FinalReferenceEsacapeExample {
    final int i;
    static FinalReferenceEsacapeExample obj;
    public  FinalReferenceEsacapeExample(){
        i = 1;                         //1
        obj = this;             //2
    }
    public static void writer(){
        new FinalReferenceEsacapeExample();
    }
    public  static void reader(){
        if(obj != null){
            int temp = obj.i;
        }
    }

}

假设一个线程A执行writer()方法,另一个线程B执行reader()方法。这里的操作2使得对象 还未完成构造前就为线程B可见。即使这里的操作2是构造函数的最后一步,且在程序中操作2 排在操作1后面,执行read()方法的线程仍然可能无法看到final域被初始化后的值,因为这里的操作1和操作2之间可能被重排序

从图3-32可以看出:在构造函数返回前,被构造对象的引用不能为其他线程所见,因为此 时的final域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到final域正确初始化之后的值。

1.4final语义在处理器中的实现  

  上面我们提到,写final域的重排序规则会要求编译器在final域的写之后,构造函数return 之前插入一个StoreStore障屏。读final域的重排序规则要求编译器在读final域的操作前面插入 一个LoadLoad屏障。由于X86处理器不会对写-写操作做重排序,所以在X86处理器中,写final域需要的 StoreStore障屏会被省略掉。同样,由于X86处理器不会对存在间接依赖关系的操作做重排序, 所以在X86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说,在X86处理器 中,final域的读/写不会插入任何内存屏障!

  JSR-133专家组增强了final的语义。通过为final域增加写和读重排序 规则,可以为Java程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在 构造函数中没有“逸出”),那么不需要使用同步(指lock和volatile的使用)就可以保证任意线程 都能看到这个final域在构造函数中被初始化之后的值。

posted @ 2019-05-19 23:17  会飞的喵星人  阅读(347)  评论(0编辑  收藏  举报