java多线程学习(九)final的内存语义

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Ditto_zhou/article/details/78738197

               final域的重排序规则

对于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域的重排序规则

写final域的操作不能重排序到构造函数之外,包含两个方面

1> JMM禁止编译器将写final域的操作重排序到构造函数外

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

writer方法的调用,首先会构造一个实例,在将这个实例赋给一个引用,假设线程B读没有重排序的话

线程A中发生 写普通域的操作重排序到构造函数外面,读线程读取构造函数的引用,并去读普通域的值,就会读取到普通域的初值,而final域由于它的重排序特性,对final域的写入并不会重排序到构造函数外,这样读线程读取构造函数的引用是,就能正确读取到final域初始化后的值。


结论就是: 在一个对象的引用对一个线程可见前,能保证final变量被正确初始化,而普通域不具有这个特性,因为普通域的写入可能会重排序到构造函数外.也就是在多线程环境下,拿到一个对象的引用后,可能会出现它的普通属性的变量还没有被正确初始化的情况.

       读final域的重排序规则

读取一个final域的引用和随后读取这个final域,不能重排序


在多线程环境下,线程A执行writer方法中,final的写重排序规则,保证final域被其他线程初始化时候一定是正确初始化的,线程B执行reader方法,如果读取final域的操作重排序到读取包含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的写入,如果想要看到,需要同步来保证内存可见性.

     final引用不能从构造函数内溢出

写final域的重排序规则保证,在引用变量为任意线程可见之前,final域已经被正确初始化了,并且还要保证:

在构造函数内部,不能让这个对象的引用对其他线程可见,也就是对象引用不能在构造函数内溢出


展开阅读全文

没有更多推荐了,返回首页