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域在构造函数中被初始化之后的值。