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

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

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

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

举个例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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

假设线程 B 当中读 obj 与读成员域之间没有重排序。

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

读 final 域的重排序规则

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

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

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

初次读引用变量 obj;

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

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

现在我们假设写线程 A 没有发生任何重排序,那么执行时序可能是:

可以看到对普通变量 i 的读取重排序到了读对象引用之前,在读普通域时候,该域还没被写线程 A 写入,这是一个错误的读取操作。而读 final 域已经被 A 线程初始化了,这个读取操作是正确的。

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

如果 final 域是引用类型

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

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

如下代码例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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 引用对象的成员域的写入。

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

代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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 域正确初始化之后的值。

【免责声明】本账号旨在介绍更多的最新信息,部分信息转载自各类纸媒、网媒之所有作品,版权归作者本人所有,转载文章目的在于分享信息、提供阅读,不代表本平台赞同其观点和对其真实性负责!若作者或版权人不愿被使用,请即与我方公众号tedu_java联系,如有侵权本账号将迅速给您回应并做处理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值