不可变对象是否不受不当发布的影响?

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)

因此 wr2 可见。


本质上,通过 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.

而设计的