不可变对象,例如字符串看起来对所有读者都具有相同的状态,无论其参考是如何获得的,即使是同步不当和缺乏事先关系.
这是通过Java 5中引入的最终字段语义实现的.通过final字段的数据访问具有更强的内存语义,如jls-17.5.1中所定义
在编译器重新排序和内存障碍方面,处理最终字段时有更多限制,见JSR-133 Cookbook.您担心的重新排序不会发生.
是的 – 双重检查锁定可以通过包装器中的最终字段完成;不需要挥发性!但这种方法不一定更快,因为需要两次读取.
请注意,此语义适用于单个最终字段,而不是整个对象.例如,String包含一个可变字段哈希;尽管如此,String被认为是不可变的,因为它的公共行为仅基于最终字段.
最终字段可以指向可变对象.例如,String.value是一个可变的char [].要求不可变对象是最终字段的树是不切实际的.
final char[] value;
public String(args) {
this.value = createFrom(args);
}
只要我们在构造函数退出后不修改值的内容,就可以了.
我们可以按任何顺序修改构造函数中的值的内容,这没关系.
public String(args) {
this.value = new char[1];
this.value[0] = 'x'; // modify after the field is assigned.
}
另一个例子
final Map map;
List list;
public Foo()
{
map = new HashMap();
list = listOf("etc", "etc", "etc");
map.put("etc", list)
}
通过最终字段的任何访问都将是不可变的,例如foo.map.get( “ETC”).得到(2).
不通过final字段访问不会–foo.list.get(2)通过不正确的发布是不安全的,即使它读取相同的目的地.
这些是设计动机.现在让我们看看JLS如何在jls-17.5.1中正式化它
冻结操作在构造函数出口处定义,与最终字段的赋值相对应.这允许我们在构造函数内的任何位置编写以填充内部状态.
不安全发布的常见问题是缺乏事前(hb)关系.即使读取看到写入,它也不会对其他行为产生任何影响.但是如果易失性读取看到易失性写入,则JMM会在许多操作中建立hb和顺序.
最后的字段语义想要做同样的事情,即使是正常的读写操作,即使是通过不安全的出版物也是如此.为此,在读取所看到的任何写入之间添加存储器链(mc)顺序.
deferences()命令限制了通过final字段访问的语义.
让我们重温Foo示例,看看它是如何工作的
tmp = new Foo()
[w] write to list at index 2
[f] freeze at constructor exit
shared = tmp; [a] a normal write
// Another Thread
foo = shared; [r0] a normal read
if(foo!=null) // [r0] sees [a], therefore mc(a, r0)
map = foo.map; [r1] reads a final field
map.get("etc").get(2) [r2]
我们有
hb(w, f), hb(f, a), mc(a, r1), and dereferences(r1, r2)
因此w对r2可见.
基本上,通过Foo包装器,一个地图(它本身是可变的)通过不安全的发布安全地发布……如果这是有道理的.
我们可以使用包装器建立最终字段语义然后丢弃它吗?喜欢
Foo foo = new Foo(); // [w] [f]
shared_map = foo.map; // [a]
有趣的是,JLS包含足以排除此类用例的条款.我猜它已被削弱,因此即使使用最终字段,也允许更多的内部线程优化.
请注意,如果在冻结操作之前泄漏,则无法保证最终字段语义.
但是,我们可以在冻结操作之后使用构造函数链接在构造函数中安全地泄漏它.
-- class Bar
final int x;
Bar(int x, int ignore)
{
this.x = x; // assign to final
} // [f] freeze action on this.x
public Bar(int x)
{
this(x, 0);
// [f] is reached!
leak(this);
}
就x而言,这是安全的;对x的冻结动作是在分配了x的构造函数的存在下定义的.这可能只是为了安全泄漏这个.