深入理解?Java?final?变量的内存模型

ImportNew 2018-11-09

(点击上方公众号,可快速关注)


来源:任春晓?


对于 final 域,编译器和处理器要遵守两个重排序规则:


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

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


举个例子:


public class FinalExample {

? ? int i;// 普通变量

? ? final int j;// final 变量

? ? static FinalExample obj;

?

? ? public FinalExample() {

? ? ? ? i = 1;// 写普通域

? ? ? ? j = 2;// 写 final 域

? ? }

?

? ? public static void writer() {// 写线程 A 执行

? ? ? ? obj = new FinalExample();

? ? }

?

? ? public static void reader() {// 读线程 B 执行

? ? ? ? FinalExample object = obj;

? ? ? ? int a = object.i;

? ? ? ? int b = object.j;

? ? }

}


这里假设一个线程 A 执行 writer ()方法,随后另一个线程 B 执行 reader ()方法。


写 final 域的重排序规则


在写 final 域的时候有两个规则:


  • JMM 禁止编译器把 final 域的写重排序到构造函数之外

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


分析上面的代码。


write 方法,只包含一行 obj = new FinalExample();,但是包含两个步骤:


  • 构造一个 FinalExample 对象

  • 把对象的引用赋值给 obj


写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。


读 final 域的重排序规则


读 final 域的重排序规则如下:


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


reader() 方法包含三个操作:


  1. 初次读引用变量 obj;

  2. 初次读引用变量 obj 指向对象的普通域 j。

  3. 初次读引用变量 obj 指向对象的 final 域 i。


读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含 这个 final 域的对象的引用。在这个示例程序中,如果该引用不为 null,那么引用 对象的 final 域一定已经被 A 线程初始化过了。


如果 final 域是引用类型


如果 final 域是引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束:


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


如下代码例子:


public class FinalReferenceExample {

? ? final int[] intArray;

? ? static FinalReferenceExample obj;

?

? ? public FinalReferenceExample() {

? ? ? ? intArray = new int[1];// 1

? ? ? ? intArray[0] = 1;// 2

? ? }

?

? ? public static void writerOne() {// A线程执行

? ? ? ? obj = new FinalReferenceExample(); // 3

? ? }

?

? ? public static void reader() {// 写线程 B 执行

? ? ? ? if (obj != null) { // 4

? ? ? ? ? ? int temp1 = obj.intArray[0]; // 5

? ? ? ? }

? ? }

}


假设首先线程 A 执行 writerOne()方法,执行完后线程 B 执行reader 方法,JMM 可以确保读线程 B 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。


避免对象引用在构造函数当中溢出


代码如下:


public class FinalReferenceEscapeExample {

? ? final int i;

? ? static FinalReferenceEscapeExample obj;

?

? ? public FinalReferenceEscapeExample() {

? ? ? ? i = 1;// 1

? ? ? ? obj = this;// 2 避免怎么做!!!

? ? }

?

? ? public static void writer() {

? ? ? ? new FinalReferenceEscapeExample();

? ? }

?

? ? public static void reader() {

? ? ? ? if (obj != null) {// 3

? ? ? ? ? ? int temp = obj.i; // 4

? ? ? ? }

? ? }

}


假设一个线程 A 执行 writer()方法,另一个线程 B 执行 reader()方法。


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


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


参考资料


  • 深入理解java内存模型


【关于投稿】


如果大家有原创好文投稿,请直接给公号发送留言。


①?留言格式:
【投稿】+《?文章标题》+?文章链接

②?示例:
【投稿】《不要自称是程序员,我十多年的?IT?职场总结》:http://blog.jobbole.com/94148/

③?最后请附上您的个人简介哈~



看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

    本站仅按申请收录文章,版权归原作者所有
    如若侵权,请联系本站删除

    区块链是什么?

    ImportNew 微信二维码

    ImportNew 微信二维码

    分享这篇文章

    ImportNew 最新文章