Are Immutable objects immune to improper publication?

时间:2016-02-03 03:05:40

标签: java multithreading immutability publish

It is an example from 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.");
        }
    }
}

On page 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;

And from 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).

From 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

My question is:

Because : (could be wrong, I don't know.)

a) the shared variable may immediately be updated before the inlined constructor initializes the object.

b) the final field will be guaranteed to be properly initialized (as visible from all threads) ONLY when the constructor returns.

Is it possible that another thread sees the default value of holder.n? (i.e. Another thread gets a reference to holder before the holder constructor returns.)

If so, then how do you explain the statement below?

Holder can be made immune to improper publication by declaring the n field to be final, which would make Holder immutable

EDIT: From JCiP. The definition of an immutable object:

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).

So, by definition, immutable objects don't have "this reference escaping" problems. Right?

But will they suffer from Out-of-order writes in double-checked-locking pattern if not declared to be volatile?

2 个答案:

答案 0 :(得分:4)

不可变对象,例如String,对于所有读者来说似乎都具有相同的状态,无论其参考是如何获得的,即使同步不当和缺乏事先关系也是如此。

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

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

是的 - 双重检查锁定可以通过包装器中的最后一个字段来完成;不需要volatile!但这种方法不一定更快,因为需要两次读取。

请注意,此语义适用于单个最终字段,而不是整个对象。例如,String包含可变字段hash;尽管如此,String被认为是不可变的,因为它的公开行为仅基于final字段。

最终字段可以指向可变对象。例如,String.valuechar[],它是可变的。要求不可变对象是最终字段的树是不切实际的。

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如何在jls-17.5.1

中形式化它

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

不安全发布的常见问题是缺乏before-before(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

答案 1 :(得分:2)

No, an immutable object can still be unsafely published if the constructor leaks a reference to this before returning (which is where the happens-before kicks in).

The two likely routes for the reference leak are if the constructor attempts to register the new object for callbacks (such as as an event listener on some constructor parameter) or with a registry or, more subtly, calls a non-final method that is overridden to do the same thing.