final修饰变量
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
当final修饰一个原生数据类型时,表示该原生数据类型的值不能发生变化;
如果final修饰一个引用类型时,表示该引用类型不能再指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。
本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
final修饰一个成员变量(属性),必须要显示初始化。
final修饰一个原生数据类型时,这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
//
可以做个试验,两种方式都不使用eclipse 都报错!如何是第一种方式初始化的,类申明属性时就初始化了,对所有线程可见了。不存在重排序(个人理解)。
//
当函数的参数类型声明为final时,说明该参数是只读型的。
JMM 详细介绍参考下文。
http://ifeve.com/java-memory-model/
存在重排序问题只能是第二种初始化。
写final域的重排序可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。
实现:
1.JMM 禁止编译器把final域的写重排序到构造函数之外。
2.编译器会在final域的写之后,构造函数return 之前,插入一个StoreStore屏障,这个屏障禁止处理器把final域的写重排序到构造函数之外。
特殊案例(代码参考上面的连接里的代码):
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经被A线程初始化过了。
现在我们假设写线程A没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:
在下图中,读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没有被写线程A写入,这是一个错误的读取操作。而读final域的重排序规则会把读对象final域的操作“限定”在读对象引用之后,此时该final域已经被A线程初始化过了,这是一个正确的读取操作。
final域为引用对象
要求:在构造函数内对一个final引用对象成员进行赋值,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
package com.qdb.thinkv.thread.base; public class FinalRefExample { final int[] intArray; static FinalRefExample obj; //构造函数 public FinalRefExample(){ //操作1 intArray=new int[1]; //操作2 intArray[0]=1; } //写线程A执行 public static void writer() { //操作3 obj=new FinalRefExample(); } //写线程B执行 public static void writerone() { //操作4 obj.intArray[0]=2; } //读线程执行 public static void reader(){ //操作5 if(obj!=null){ //操作6 int temp1=obj.intArray[0]; } } }
这里假设一种情况
线程A执行完成以后执行线程B执行完成以后执行线程C
A操作对C可见,B操作对C不可见,可能出现这样的一种情况
如果想要求确保线程C看到线程B对数组元素的写入,写线程B和读线程C之间需要使用同步原语(lock 或 volatile)来确保内存可见性。
为什么final引用不能从构造函数内逸出
package com.qdb.thinkv.thread.base; public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; //构造函数 public FinalReferenceEscapeExample(){ //操作1 i=1; //操作2 obj=this; } //写线程A执行 public static void writer() { //操作3 obj=new FinalReferenceEscapeExample(); } //读线程执行 public static void reader(){ //操作5 if(obj!=null){ //操作6 int temp1=obj.i; } } public static void main(String[] args) { } }
因为操作1和操作2之间可能重排序(疑惑点。可能吗?根据“程序顺序执行”,一个线程中的每一个操作 happenbefore于该线程中的人任意后续操作 )
Java并发编程——this引用逸出("this" Escape)
Java 事件 基础
从上图可以看出:在构造函数返回前,被构造对象的引用不能为其他线程所见,因为此时的final域可能没有被初始化。在构造函数返回后,任意线程都将保证能看到final域正确初始化之后的值。