对于final域,编译器和处理器要遵守两个重排序规则
1> 在构造函数内对一个final域的写入,与随后把这个构造函数的引用赋值给一个引用变量,两个操作不能重排序
2> 初次读一个包含final域对象的引用,和随后初次读这个final域,这两个操作不能重排序
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 read(){//线程B读执行
FinalExample fe = obj;//读取包含final域对象的引用
int a = fe.i;//读取普通变量
int b = fe.j;//读取final变量
}
}
写final域的操作不能重排序到构造函数之外,包含两个方面
1> JMM禁止编译器将写final域的操作重排序到构造函数外
2> 编译器会在final域的写入之后,构造函数return前,插入一个StoreStore屏障,这个屏障禁止处理器把final域的写重排序到构造函数之外
writer方法的调用,首先会构造一个实例,在将这个实例赋给一个引用,假设线程B读没有重排序的话
线程A中发生 写普通域的操作重排序到构造函数外面,读线程读取构造函数的引用,并去读普通域的值,就会读取到普通域的初值,而final域由于它的重排序特性,对final域的写入并不会重排序到构造函数外,这样读线程读取构造函数的引用是,就能正确读取到final域初始化后的值。
结论就是: 在一个对象的引用对一个线程可见前,能保证final变量被正确初始化,而普通域不具有这个特性,因为普通域的写入可能会重排序到构造函数外.也就是在多线程环境下,拿到一个对象的引用后,可能会出现它的普通属性的变量还没有被正确初始化的情况.
读取一个final域的引用和随后读取这个final域,不能重排序
在多线程环境下,线程A执行writer方法中,final的写重排序规则,保证final域被其他线程初始化时候一定是正确初始化的,线程B执行reader方法,如果读取final域的操作重排序到读取包含final域的对象的引用之前,final变量都还没有被初始化,这是一个错误的读取操作,显然,当final引用读取之后,如果这个引用不为空,能够保证final变量被初始化过,这个读取就没有问题
结论:多线程环境下,final域的读取操作会重排序读取在包含final域的引用之后,但是普通域的读取操作可能排在,引用的前面。
当final域是引用类型时,写final域的重排序规则对编译器和处理器增加下面约束:在构造函数内对一个final引用的对象的成员域的写入,和随后把这个构造函数的引用赋给一个引用变量,这两者之间不能重排序
class FinalReferenceExample{
final int[] intArray;//为引用类型的final
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 writerTwo(){//写线程B执行
obj.intArray[0]=2;//4
}
public static void reader(){//读线程C执行
if(obj!=null){//5
int temp = obj.intArray[0];//6
}
}
现在假设一种可能,写线程A执行完毕,写线程B执行,读线程C执行
写线程A执行,根据前面final域的重排序规则,操作1对final域的写入和操作2对final域的写入,不会重排序到操作3对象的引用赋给一个引用变量后面,也就是读线程C至少可以看到 intArray[0]为1,
而线程B的写入和线程C存在数据竞争,读线程C可能看不到线程B对intArray的写入,如果想要看到,需要同步来保证内存可见性.
在构造函数内部,不能让这个对象的引用对其他线程可见,也就是对象引用不能在构造函数内溢出