Java中被final修饰的变量与普通变量有何区别?被final修饰的变量不可更改、被final修饰的方法不可重写是怎样做到的?带着疑问我们一点点拨开云雾。
一、final的内存定义及规则
对于final关键字,编译器、处理器从读写两个角度限制了其使用规则:
- 对于一个类的final修饰的变量,如果在定义是不指定初始值,那么在构造函数中必须进行初始化,在构造函数中进行final域的写入时,随后将构造后的对象引用赋值给另外一个引用变量,它们之间不能进行重排序的发生。
- 在读一个包含final关键字的对象引用和读这个引用的包含的final修饰的变量时,这两个操作间不能发生重排序。
下面通过一段代码分析一下具体场景:
public
这里先假设A线程执行finalWriter方法,B线程执行finalRead()方法,通过上述对于final的规则描述我们分析一下finalWriter方法的执行流程:
- 构造一个FinalTest对象
- 将构造后的对象引用进行赋值
对于final修饰的变量进行赋值操作时的重排序规则如下:
1、Java内存模型禁止将对final关键字修饰的变量进行写操作重排序到构造函数之外。
2、编译器会在写之后,构造函数return之前插入StoreStore屏障,这个屏障确保编译器不会把final变量写操作重排序到构造函数之外。
下面假定一种重排序的场景如下图所示:
上图的场景情况为变量i为普通变量,在进行赋值时发生了重排序(由于这时候有可能构造函数还未完成),在构造函数结束后,才进行了赋值,线程B读取到的i的值为赋值前的初始值0,而对于final修饰的变量j由于禁止重排序,在构造函数return前需要进行赋值,限定到了构造函数内,读取到的变量j为正确的值。
然后再分析一下执行finalRead()方法的流程:
- 读引用变量Obj将其赋值给object变量。
- 读引用的普通变量i。
- 读引用的final修饰的变量j
对于final修饰的读操作重排序规则:
在一个线程中首次读对象的引用和首次读该对象包含的final修饰的变量,Java内存模型禁止重排序(也就是说在读取一个final修饰的变量前,一定是先获取该变量对应的引用),主要原理就是在读取final修饰的变量前插入LoadLoad屏障。
假设上述情况,线程A正常执行,变量i没有发生重排序的情况,而对于线程B读取变量i和读对象的引用发生了重排序,如下图所示
读对象的普通变量i时处理器发生了重排序,读变量在读对象的引用之前发生,这时候变量还未开始进行赋值,而对于final修饰的变量j来说,由于其遵循重排序规则(读变量首先要读变量对应的对象引用),所以读取的值是正确的。
除了上述两种场景之外,假设在构造函数内使用this关键字将当前对象赋值给成员变量(逸出),如下代码所示:
public
这时候同样有两个线程,一个线程执行finalWriter,另外一个执行finaRead,也有可能会出现被final修饰的变量j没有进行赋值的情况。
参考《Java并发编程的艺术》