不可变对象是否不受不当发布的影响?
Are Immutable objects immune to improper publication?
这是来自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] The problem here is not the Holder class itself, but that the
Holder is not properly published. However, Holder can be made immune
to improper publication by declaring the n field to be final, which
would make Holder immutable;
并且来自 this answer:
the specification for final (see @andersoj's answer) guarantees that
when the constructor returns, the final field will have been properly
initialized (as visible from all threads).
来自 wiki:
For example, in Java if a call to a constructor has
been inlined then the shared variable may immediately be updated once
the storage has been allocated but before the inlined constructor
initializes the object
我的问题是:
因为:(可能是错的,我不知道。)
a) 在内联构造函数初始化对象之前,可以立即更新共享变量。
b) 只有在构造函数 returns.
时,final 字段才能保证正确初始化(从所有线程可见)
是否有可能另一个线程看到了 holder.n
的默认值? (即另一个线程在 holder
构造函数 returns 之前获取对 holder
的引用。)
如果是这样,那么你如何解释下面的说法?
Holder can be made immune to improper publication by declaring the n
field to be final, which would make Holder immutable
编辑:
来自 JCiP。不可变对象的定义:
An object is immutable if:
x Its state cannot be modified after
construction;
x All its fields are final;[12] and
x It is properly
constructed (the this reference does not escape during construction).
因此,根据定义,不可变对象没有“this
引用转义”问题。对吗?
但是如果不声明为 volatile,它们是否会受到 Out-of-order writes 双重检查锁定模式的影响?
不,如果构造函数在返回之前泄漏了对 this
的引用(这是 happens-before 开始的地方),那么仍然可以不安全地发布不可变对象。
引用泄漏的两种可能途径是构造函数尝试为回调注册新对象(例如某些构造函数参数上的事件侦听器)或使用注册表,或者更巧妙地调用非被重写以执行相同操作的 final 方法。
不可变对象,例如String
,对于所有读者来说似乎都具有相同的状态,无论其引用是如何获得的,即使同步不当且缺少 happens-before 关系也是如此。
这是通过 Java 中引入的 final
字段语义实现的 5. 通过 final 字段的数据访问具有更强的内存语义,如 jls-17.5.1
中所定义
在编译器重排序和内存屏障方面,在处理 final 字段时有更多约束,请参阅 JSR-133 Cookbook。您担心的重新排序不会发生。
是的——双重检查锁定可以通过包装器中的 final 字段来完成;不需要 volatile
!但是这种方式不一定更快,因为需要两次读取。
请注意,此语义适用于单个最终字段,而不适用于整个对象。例如,String
包含一个可变字段 hash
;然而,String
被认为是不可变的,因为它的 public 行为仅基于 final
字段。
final 字段可以指向可变对象。例如,String.value
是可变的 char[]
。要求不可变对象是一棵 final 字段树是不切实际的。
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)
.
通过 final 字段访问 not 并不安全 -- foo.list.get(2)
通过不正确的 publication 是不安全的,即使它读取相同的目的地。
这些就是设计动机。现在让我们看看JLS如何在jls-17.5.1
中形式化它
A freeze
动作在构造函数出口处定义,与在 final 字段的赋值处相对。这允许我们在构造函数中的任何地方写入以填充内部状态。
不安全 publication 的常见问题是缺少 happens-before (hb
) 关系。即使读取看到写入,它也不会建立 w.r.t 其他操作。但是,如果易失性读取看到易失性写入,JMM 会在许多操作中建立 hb
和顺序。
final
字段语义想要做同样的事情,即使是正常的读写,也就是说,即使是通过不安全的 publications。为此,在读取看到的任何写入之间添加一个内存链 (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
包装器,虽然不安全 publication... 可以安全地发布地图(其本身是可变的)...如果这有意义。
我们可以使用包装器建立最终字段语义然后丢弃它吗?喜欢
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] The problem here is not the Holder class itself, but that the Holder is not properly published. However, Holder can be made immune to improper publication by declaring the n field to be final, which would make Holder immutable;
并且来自 this answer:
the specification for final (see @andersoj's answer) guarantees that when the constructor returns, the final field will have been properly initialized (as visible from all threads).
来自 wiki:
For example, in Java if a call to a constructor has been inlined then the shared variable may immediately be updated once the storage has been allocated but before the inlined constructor initializes the object
我的问题是:
因为:(可能是错的,我不知道。)
a) 在内联构造函数初始化对象之前,可以立即更新共享变量。
b) 只有在构造函数 returns.
时,final 字段才能保证正确初始化(从所有线程可见)是否有可能另一个线程看到了 holder.n
的默认值? (即另一个线程在 holder
构造函数 returns 之前获取对 holder
的引用。)
如果是这样,那么你如何解释下面的说法?
Holder can be made immune to improper publication by declaring the n field to be final, which would make Holder immutable
编辑: 来自 JCiP。不可变对象的定义:
An object is immutable if:
x Its state cannot be modified after construction;x All its fields are final;[12] and
x It is properly constructed (the this reference does not escape during construction).
因此,根据定义,不可变对象没有“this
引用转义”问题。对吗?
但是如果不声明为 volatile,它们是否会受到 Out-of-order writes 双重检查锁定模式的影响?
不,如果构造函数在返回之前泄漏了对 this
的引用(这是 happens-before 开始的地方),那么仍然可以不安全地发布不可变对象。
引用泄漏的两种可能途径是构造函数尝试为回调注册新对象(例如某些构造函数参数上的事件侦听器)或使用注册表,或者更巧妙地调用非被重写以执行相同操作的 final 方法。
不可变对象,例如String
,对于所有读者来说似乎都具有相同的状态,无论其引用是如何获得的,即使同步不当且缺少 happens-before 关系也是如此。
这是通过 Java 中引入的 final
字段语义实现的 5. 通过 final 字段的数据访问具有更强的内存语义,如 jls-17.5.1
在编译器重排序和内存屏障方面,在处理 final 字段时有更多约束,请参阅 JSR-133 Cookbook。您担心的重新排序不会发生。
是的——双重检查锁定可以通过包装器中的 final 字段来完成;不需要 volatile
!但是这种方式不一定更快,因为需要两次读取。
请注意,此语义适用于单个最终字段,而不适用于整个对象。例如,String
包含一个可变字段 hash
;然而,String
被认为是不可变的,因为它的 public 行为仅基于 final
字段。
final 字段可以指向可变对象。例如,String.value
是可变的 char[]
。要求不可变对象是一棵 final 字段树是不切实际的。
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)
.
通过 final 字段访问 not 并不安全 -- foo.list.get(2)
通过不正确的 publication 是不安全的,即使它读取相同的目的地。
这些就是设计动机。现在让我们看看JLS如何在jls-17.5.1
中形式化它A freeze
动作在构造函数出口处定义,与在 final 字段的赋值处相对。这允许我们在构造函数中的任何地方写入以填充内部状态。
不安全 publication 的常见问题是缺少 happens-before (hb
) 关系。即使读取看到写入,它也不会建立 w.r.t 其他操作。但是,如果易失性读取看到易失性写入,JMM 会在许多操作中建立 hb
和顺序。
final
字段语义想要做同样的事情,即使是正常的读写,也就是说,即使是通过不安全的 publications。为此,在读取看到的任何写入之间添加一个内存链 (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
包装器,虽然不安全 publication... 可以安全地发布地图(其本身是可变的)...如果这有意义。
我们可以使用包装器建立最终字段语义然后丢弃它吗?喜欢
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
.