5.2.4 final
在JMM中要求final域(属性)的初始化动作必须在构造方法return之前完成。换言之,一个对象创建以及将其赋值给一个引用是两个动作,对象创建还需要经历分配空间和属性初始化的过程,普通的属性初始化允许发生在构造方法return之后(指令重排序)。
似乎这个问题变得很可怕,因为在Java程序中得到的对象竟然有可能还没有执行完构造方法内的属性赋值,但在大部分情况下,对象的使用都是在线程内部定义的,在单线程中是绝对可靠的,或者说在单线程中要求使用对象引用时,该对象已经被初始化好。但如果在此过程中有另一个线程通过这个未初始化好的对象引用读取相应的属性,那么就可能读取到的并不是真正想要的值。在Java中final可以保证这一点,所以它可以避免这种类型的逃逸问题。
但是它并不能完全解决所有的逃逸问题,而只是确保在构造方法return以前是会被初始化的,无法确保不与其他的指令进行重排序,比如下面的代码:
01 |
private static TestObject testObject = null; |
05 |
testObject = this; //这个地方可能和a=100发生指令重排序 |
07 |
public static void read() { |
08 |
if(testObject != null) { |
如果有另一个线程调用静态方法read(),则可能得到testObject非空值,而此时有可能a=100这个动作还未执行(因为它可以与testObject
= this进行重排序),那么操作的数据就将是错误的。
进一步探讨:如果final所修饰的不是普通变量,而是数组、对象,那么它能保证自己本身的初始化在其外部对象的构造方法返回之前,但是它本身作为对象,对内部的属性是无法保证的。如果是某些具有标志性的属性,则需要根据实际情况做进一步处理,才可以达到线程安全的目的。
经过JSR-133对final进行语义增强后,我们就可以比较放心地使用final语法了。但是我们想看看构造方法还没做完,变量是什么样子呢?普通变量和final变量到底又有什么区别呢?下面我们就写一段和并发编程没多大关系的代码来跑一跑看看。
代码清单5-7 构造方法未结束,看看属性是什么样子
01 |
public class FinalConstructorTest { |
03 |
static abstract class A { |
09 |
public abstract void display(); |
12 |
static class B extends A { |
14 |
private int INT = 100; |
16 |
private final int FINAL_INT = 100; |
18 |
private final Integer FINAL_INTEGER = 100; |
20 |
private String STR1 = "abc"; |
22 |
private final String FINAL_STR1 = "abc"; |
24 |
private final String FINAL_STR2
= new String("abc"); |
26 |
private final List<String> FINAL_LIST
= new ArrayList<String>(); |
30 |
System.out.println("abc"); |
33 |
public void display() { |
34 |
System.out.println(INT); |
35 |
System.out.println(FINAL_INT); |
36 |
System.out.println(FINAL_INTEGER); |
37 |
System.out.println(STR1); |
38 |
System.out.println(FINAL_STR1); |
39 |
System.out.println(FINAL_STR2); |
40 |
System.out.println(FINAL_LIST); |
44 |
public static void main(String []args) { |
在这段代码中,我们跳开了构造方法返回之前对final的初始化动作,而是在构造方法内部去输出这些final属性。这段代码的输出结果可能会让我们意想不到,大家可以自行测试,如果在测试过程中使用断点跟踪去查看这些数据的值,则可能在断点中看到的值与实际输出的值还会有所区别,因为断点也是通过另一个线程去看对象的属性值的,看到的对象可能正好是没有初始化好的对象。
#Java工程师#