- 写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; // 读final 域
}
}
比如 writer()里obj = new FinalExample(); 包含两个步骤:
这时写普通域操作被编译器排序到构造器之外:
- 读final域重排序规则 -
读final域的重排序,在一个线程中,初次读对象引用与初次读对象包含final域, JMM禁止处理器重排序这两个操作(这个规则仅仅针对处理器)。编译器会在读final操作前面插入一个loadload屏障。
初次读对象引用与初次读该对象包含final域,这两个操作之间存在间接依赖关系, 由于编译器遵守间接依赖关系。因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,也不会重排序这两个操作,但少数处理器允许存在间接依赖关系的操作做重排序(alpha处理器), 这个规则专门用来针对这种处理器。
reader()包含3个操作:
读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没被写线程写入,这是一个错误的读取操作, 而读final域的重排序规则会把读对象final域的操作“限定”在读对象引用之后, 此时刻final域已经被现场初始化过,这是一个正确的读取操作。
读final域的重排序规则可以确保,在读一个对象的final域之前, 一定会先读包含这个final域的对象引用。
- final 域为引用类型 -
对于应用类型, 写final域重排序规则对编译器和处理器增加约束:
在构造函数内对一个final引用的对象的成员域的写入, 与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量。
public class FinalReferenceExample {
final int[] intArray; // final 是引用类型
static FinalReferenceExample obj;
public FinalReferenceExample() { // 构造函数
intArray = new int[1];
intArray[0] = 1;
}
public static void writeOne(){ // 写线程A执行
obj = new FinalReferenceExample();
}
public static void writeTwo(){ // 写线程B执行
obj.intArray[0] = 2;
}
public static void reader(){ // 读线程C执行
if(obj != null){
int temp1 = obj.intArray[0];
}
}
}
前提
假设线程A先执行writeOne() ,执行完后线程B执行writeTwo() 或 线程C执行reader() :
结论:1,3 不能重排序, 2,3 不能重排序
说明
线程B和线程C是竞争关系。JMM可以确保线程C至少能看到线程A在final引用对象的成员域的写入,即C至少能看到数组下标0的值为1. 线程B对数据的写入, 线程C 可能看到也可能看不到。JMM不保证线程B的写入对线程C可见,因为写线程B和读线程C之间存在数据竞争。
如果想确保线程C看到些线程对数据元素的写入, 线程B和线程C之间需要使用同步原语(lock或volatile) 来确保内存可见性。
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample() {
i = 1; // 1 写final域
obj = this; // 2 this引用在此"逸出"
}
public static void writer() {
new FinalReferenceEscapeExample();
}
public static void reader() {
if(obj != null){
int temp = obj.i;
}
}
}
说明:
线程A执行writer()方法,线程B执行reader方法。这里的操作2 (this引用在此”逸出”) 使得对象还未完成构造前线程B可见,无法看到final域被初始化后的值。因为这里的操作1 (i=1) 和操作2(obj = this) 之间可能被重排序。
在构造函数返回前, 被构造对象的引用不能为其他线程所见,因为此时的final域可能还没有被初始化。在构造函数返回后, 任意线程都将保证看到final域正确初始化之后的值。
final语义在处理器中实现
X86处理器不会对写-写操作做重排序, 在X86处理器中,写final域需要的storestore屏障被省略掉。由于X86处理器不会对存在间接依赖关系的操作做重排序。所以在X86处理器中,读final域需要的loadload屏障也会被省略掉。
end: 在X86处理器中 final域的读/写不会插入任何内存屏障
- 作者介绍 -
若水 架构师一枚,现就职于小米小爱开放平台,一个才貌双全的美女码农,平常喜欢总结知识和刷算法题,经常参加LeetCode算法周赛。