Welcome 微信登录

首页 / 软件开发 / JAVA / 深入理解Java内存模型(六) final

深入理解Java内存模型(六) final2014-05-31 infoq 程晓明与前面介绍的锁和volatile相比较,对final域的读和写更像是普通的变量访问。对于final域,编译 器和处理器要遵守两个重排序规则:

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

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

下面,我们通过一些示例性的代码来分别说明这两个规则:

public class FinalExample {int i;//普通变量final int j;//final变量static FinalExample obj; public void 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;//读final域}}
这里假设一个线程A执行writer ()方法,随后另一个线程B执行reader ()方法。下面我们通过 这两个线程的交互来说明这两个规则。

写final域的重排序规则

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

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

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

现在让我们分析writer ()方法。writer ()方法只包含一行代码:finalExample = new FinalExample ()。这行代码包含两个步骤:

构造一个FinalExample类型的对象;

把这个对象的引用赋值给引用变量obj。

假设线程B读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要这个假设),下图是 一种可能的执行时序:

在上图中,写普通域的操作被编译器重排序到了构造函数之外,读线程B错误的读取了普通变量i初始 化之前的值。而写final域的操作,被写final域的重排序规则“限定”在了构造函数之内,读线程B正确 的读取了final变量初始化之后的值。

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