final字段与构建线程安全的实例
使用final关键字声明的字段只能被初始化一次。final字段在创建不允许被改变的对象时起到了非常重要的作用。
final字段的初始化只能在“字段声明时”或是“构造函数中”进行。那么,当final字段的初始化结束后,无论在任何时候,它的值对其他线程都是可见的。Java内存模型可以确保被初始化后的final字段在构造函数的处理结束后是可见的。也就是说,可以确保以下事情:
- 如果构造函数的处理结束了
final字段初始化后的值对所有线程都是可见的
在final字段可以追溯到的所有范围内都可以看到正确的值
- 在构造函数的处理结束前
可能会看到final字段的值是默认的初始值(0、false或是null)
java.util.concurrent.ConcurrentHashMap类使用final和volatile特性实现了无阻塞的Map。
将常量字段设置为final
Java内存模型可以确保final字段在构造函数执行结束后可以正确地被看到。这样就不再需要通过synchronized和volatile进行同步了。因此,将不希望被改变的字段设为final。
不要从构造函数中泄露this
在构造函数执行结束前,可能会看到final字段的值发生变化。也就是说,存在首先看到“默认初始值”,然后看到“显示地初始化的值”的可能性。
1 | // 可能会显示出0 |
根据Java内存模型,这时看到的last.x的值可能会是0。因为线程B在print方法中看到的last的值,是在构造函数处理结束前获取的this。
Java内存模型可以确保构造函数处理结束时final字段的值被正确地初始化,对其他线程是可见的。总而言之,如果使用通过new Something()获取的实例,final字段是不会发生可见性问题的。但是,如果在构造函数的处理过程中this还没有创建完毕,就无法确保final字段的正确的值对其他线程是可见的。
代码修改后,就不可能会显示出0了。这里进行的具体修改如下:
- 将构造函数修改为private,让外部无法调用
- 编写一个名为create的静态方法,在其中使用new关键字创建实例
- 将静态字段last赋值为上面使用new关键字创建的实例
这样修改后,只有当构造函数处理结束后静态字段last才会被赋值,因此可以确保final字段被正确地初始化。
1 | // 不会显示出0 |
在构造函数中将静态字段赋值为this是非常危险的。因为其他线程可能会通过这个静态字段访问正在创建中的实例。同样地,向静态字段中保存的数组和集合中保存this也是非常危险的。
另外,在构造函数中进行方法调用时,以this为参数的方法调用也是非常危险的。因为该方法可能会将this放在其他线程可以访问到的地方。