不可变对象是否对不正当的出版物免疫?

这是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字段为最终字段,Holder可以免于不正当的出版物,这将使Holder不可改变;

从这个回答 :

final的规范(参见@ andersoj的答案)保证当构造函数返回时,最终字段将被正确初始化(从所有线程可见)。

来自维基 :

例如,在Java中,如果内联对构造函数的调用,那么一旦分配了存储但在内联构造函数初始化对象之前,共享变量可能会立即更新

我的问题是:

因为:(可能是错的,我不知道。)

a)在内联构造函数初始化对象之前,可以立即更新共享变量。

b)只有当构造函数返回时,才能保证最终字段被正确初始化(从所有线程可见)。

是否有可能另一个线程看到holder.n的默认值? (即另一个线程在holder构造函数返回之前获取对holder的引用。)

如果是这样,那你如何解释下面的陈述?

通过声明n字段是最终的,可以使持有者免于不正确的出版物,这将使Holder不可变

编辑:来自JCiP。 不可变对象的定义:

如果对象是不可变的,则:
x施工后不能修改其状态;

x它的所有领域都是最终的; [12]和

x它是正确构造的(该参考在施工期间不会逃逸)。

因此,根据定义,不可变对象不具有“ this引用转义”问题。 对?

但如果没有声明为volatile,它们是否会遭遇双重检查锁定模式的无序写入 ?

对于所有读者来说,不可变对象(例如String )似乎具有相同的状态,无论其参考是如何获得的,即使在不正确​​的同步和缺乏先发生关系的情况下也是如此。

这是通过Java 5中引入的final字段语义实现的。通过final字段的数据访问具有更强的内存语义,如jls-17.5.1中所定义。

在编译器重新排序和内存障碍方面,处理final字段时有更多限制,请参阅JSR-133 Cookbook 。 你担心的重新排序不会发生。

是的 – 双重检查锁定可以通过包装器中的最终字段完成; 不需要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) } 

通过 final字段的任何访问都将是不可变的,例如foo.map.get("etc").get(2)

通过final字段访问不会 foo.list.get(2)通过不正确的发布是不安全的,即使它读取相同的目的地。


这些是设计动机。 现在让我们看看JLS如何在jls-17.5.1中将其正式化

freeze操作在构造函数出口处定义,与最终字段的赋值相对应。 这允许我们在构造函数内的任何位置编写以填充内部状态。

不安全发布的常见问题是缺乏事前( hb )关系。 即使读取看到写入,它也不会建立任何其他操作。 但是如果易失性读取看到易失性写入,则JMM会在许多操作中建立hb和顺序。

final字段语义想要做同样的事情,即使是正常的读写操作,即使是通过不安全的出版物也是如此。 为此,在读取所看到的任何写入之间添加存储器链( 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) 

因此wr2可见。


基本上,通过Foo包装器,一个地图(它本身是可变的)通过不安全的发布安全地发布……如果这是有道理的。

我们可以使用包装器建立最终字段语义然后丢弃它吗? 喜欢

 Foo foo = new Foo(); // [w] [f] shared_map = foo.map; // [a] 

有趣的是,JLS包含足以排除此类用例的条款。 我猜它已被削弱,因此即使使用最终字段,也允许更多的内部线程优化。


请注意,如果在冻结操作之前泄漏,则无法保证最终字段语义。

但是,我们可以在冻结操作之后使用构造函数链接在构造函数中安全地泄漏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

不,如果构造函数在返回之前泄漏this的引用(这是在发生之前发生的地方),则仍然可以不安全地发布不可变对象。

引用泄漏的两个可能路由是构造函数是否尝试为回调注册新对象(例如作为某个构造函数参数上的事件侦听器)或注册表,或者更巧妙地调用被覆盖的非final方法做同样的事情。