Java 不可变对象是否对不正确的发布免疫?
这是我们的一个例子 第34页: [15] 这里的问题不是Holder类本身,而是 持有者未正确发布。然而,持有者可以免疫 通过将n字段声明为final来阻止不正确的发布 会使持有者不可变 和来自: 最终规范(见@andersoj的答案)保证 当构造函数返回时,最后一个字段将被正确地删除 已初始化(从所有线程可见) 发件人: 例如,在Java中,如果对构造函数的调用 如果已内联,则共享变量可能会立即更新一次 存储已分配,但在内联构造函数之前 初始化对象 我的问题是: 因为:(可能是错的,我不知道。) a) 在内联构造函数初始化对象之前,可以立即更新共享变量 b) 只有当构造函数返回时,才能保证正确初始化最后一个字段(从所有线程可见) 另一个线程是否可能看到默认值Java 不可变对象是否对不正确的发布免疫?,java,multithreading,immutability,publish,Java,Multithreading,Immutability,Publish,这是我们的一个例子 第34页: [15] 这里的问题不是Holder类本身,而是 持有者未正确发布。然而,持有者可以免疫 通过将n字段声明为final来阻止不正确的发布 会使持有者不可变 和来自: 最终规范(见@andersoj的答案)保证 当构造函数返回时,最后一个字段将被正确地删除 已初始化(从所有线程可见) 发件人: 例如,在Java中,如果对构造函数的调用 如果已内联,则共享变量可能会立即更新一次 存储已分配,但在内联构造函数之前 初始化对象 我的问题是: 因为:(可能是错的,我不知道。
holder.n
?(即,在holder
构造函数返回之前,另一个线程获取对holder
的引用。)
如果是,那么你如何解释下面的陈述
持有者可以通过声明n来避免不适当的发布
字段为最终字段,这将使持有者不可变
编辑:
来自JCiP。不可变对象的定义:
如果:x之后无法修改其状态 建设 x其所有字段均为最终字段;[12] 及 它是正确的 已构造(此引用在构造期间不会消失) 因此,根据定义,不可变对象不存在“
this
reference-escaping”问题。对吧?
但是如果不声明为volatile,它们会在双重检查锁定模式中受到影响吗?否,如果构造函数在返回之前泄漏了对该的引用,则仍然可以安全地发布不可变对象(这是启动之前发生的情况)
引用泄漏的两种可能途径是,如果构造函数尝试注册新对象以进行回调(例如,某个构造函数参数上的事件侦听器),或者使用注册表,或者更微妙地,调用被重写的非final方法以执行相同的操作 一个不可变的对象,例如
字符串
,对于所有读卡器来说似乎都具有相同的状态,而不管它的引用是如何获得的,即使同步不正确且缺少关系也是如此
这是通过Java5中引入的final
字段语义实现的。如中所定义,通过最终字段进行的数据访问具有更强的内存语义
在编译器重新排序和内存障碍方面,在处理最终字段时有更多约束,请参阅。你担心的重新排序不会发生
是的——可以通过包装中的最后一个字段进行双重检查锁定;不需要volatile
!但这种方法不一定更快,因为需要两次读取
请注意,此语义适用于单个最终字段,而不是整个对象。例如,
String
包含一个可变字段hash
;然而,String
被认为是不可变的,因为它的公共行为仅基于final
字段
最后一个字段可以指向可变对象。例如,String.value
是一个可变的char[]
。要求不可变对象是最终字段树是不切实际的
final char[] value;
public String(args) {
this.value = createFrom(args);
}
只要我们在构造函数退出后不修改value
的内容,就可以了
我们可以在构造函数中以任何顺序修改value
的内容,这无关紧要
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”).get(2)
非通过最终字段访问不--foo.list.get(2)
通过不正确的发布是不安全的,即使它读取相同的目标
这就是设计动机。现在,让我们看看JLS是如何在
冻结
操作在构造函数出口处定义,与分配最终字段时相同。这允许我们在构造函数中的任意位置写入以填充内部状态
不安全发布的常见问题是缺少before(hb
)关系。即使读看到写,它也不会建立任何其他操作。但是,如果一个volatile read看到一个volatile write,JMM将在许多操作中建立hb
和一个顺序
final
字段语义想要做同样的事情,即使是正常的读写,也就是说,即使是通过不安全的发布。为此,在读操作看到的任何写操作之间添加内存链(mc
)顺序
deferences()
顺序将语义限制为通过最后一个字段访问
让我们重温一下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
wrapper,一个映射(其本身是可变的)可以通过不安全的发布安全地发布。。。如果这有道理的话
我们可以使用包装器建立最终字段语义,然后丢弃它吗?像
Foo foo = new Foo(); // [w] [f]
shared_map = foo.map; // [a]
有趣的是,JLS包含了足够多的条款来排除这种用例。我猜它被削弱了,因此允许进行更多的内线程优化,即使使用final字段也是如此
final char[] value;
public String(args) {
this.value = createFrom(args);
}
请注意,如果在冻结操作之前泄漏了此,则最终字段语义为n
-- 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);
}