如何解决不可变物体是否可以不受不当出版的影响?
一个不可变的对象,例如String
,对于所有读者来说似乎都具有相同的状态,而不管它的引用如何获得,即使同步不正确并且没有先发生后关系。
这是通过final
Java5中引入的字段语义实现的。通过最后一个字段进行的数据访问具有更强的内存语义,如jls-17.5.1中所定义。
在编译器重新排序和内存障碍方面,处理最终字段时存在更多约束,请参阅《JSR-133 Cookbook》。您担心的重新排序不会发生。
是的-可以通过包装器中的final字段进行双重检查锁定;没有volatile
要求!但是这种方法不一定更快,因为需要两次读取。
请注意,此语义适用于各个最终字段,而不适用于整个对象。例如,String
包含一个可变字段hash
;但是,String
由于其公共行为仅基于final
字段,因此被认为是不变的。
final字段可以指向可变对象。例如,String.value
a 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)
}
final字段 的任何访问都将是不可变的,例如foo.map.get("etc").get(2)
。
不 通过最终字段进行访问 不会 - foo.list.get(2)
通过不正确的发布是不安全的,即使它读取的是同一目的地。
这些就是设计动机。现在让我们看看JLS如何在jls-17.5.1中对其进行形式化
freeze
在构造函数的出口处定义了一个动作,与在final字段的分配中定义的动作相同。这使我们可以在构造函数内部的任何地方编写以填充内部状态。
不安全发布的常见问题是缺少“事前”(hb
)关系。即使读取看到写入,它也不会与其他操作建立任何关系。但是,如果易失性读取看到易失性写入,则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
包装器,可以通过不安全的发布安全地发布地图(本身是可变的)……如果这是有道理的。
我们可以使用包装器建立最终字段的语义,然后丢弃它吗?喜欢
Foo foo = new Foo(); // [w] [f]
shared_map = foo.map; // [a]
有趣的是,JLS包含足以排除此类用例的子句。我猜想它已经被削弱了,所以允许更多的内部线程优化,即使对于最终字段也是如此。
注意,如果this
在冻结操作之前泄漏了,则不能保证最终字段的语义。
但是,通过冻结构造函数,我们 可以 在冻结动作 之后 安全地泄漏this
到构造函数中。 __
-- 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
分配了构造函数的地方定义的。这可能只是为了安全泄漏而设计的this
。
解决方法
这是来自JCiP的示例。
public class Unsafe {
// Unsafe publication
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
}
public class Holder {
private int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if (n != n) {
throw new AssertionError("This statement is false.");
}
}
}
在第34页上:
[15]这里的问题不是Holder类本身,而是Holder没有正确发布。但是,可以通过将n字段声明为final来使Holder免受不适当发布的影响,这将使Holder不变。
从这个答案:
final的规范(请参阅@andersoj的答案)保证当构造函数返回时,将对final字段进行适当的初始化(从所有线程可见)。
从维基:
例如,在Java中,如果已经内联了对构造函数的调用,则一旦分配了存储空间,但是在内联的构造函数初始化对象之前,可以立即更新共享变量。
我的问题是:
因为:(可能不正确,我不知道。)
a)可以在内联构造函数初始化对象之前立即更新共享变量。
b)只有在构造函数返回时,才能保证对final字段进行正确的初始化(从所有线程可见)。
另一个线程是否可能看到默认值holder.n
?(即,另一个线程holder
在holder
构造函数返回之前获取对它的引用。)
如果是这样,那么您如何解释以下声明?
通过将n字段声明为final,可以使Holder免受不适当发布的影响,这将使Holder不可变
编辑: 从JCiP。不可变对象的定义:
在以下情况下对象是不可变的:
x构造后无法修改其状态;x其所有字段均为最终字段; [12]和
x构造正确(该参考在构造期间不会逸出)。
因此,根据定义,不可变对象没有“ this
引用转义”问题。对?
但是如果不声明为易失性,它们是否会遭受双重检查锁定模式的乱序写入?
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。