【问题标题】:Are Immutable objects immune to improper publication?不可变对象是否不受不当发布的影响?
【发布时间】:2016-02-03 03:05:40
【问题描述】:

这是来自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 类本身,而是 持有人未正确发布。然而,持有人可以免疫 通过将 n 字段声明为最终字段来进行不当发布,这 会使 Holder 不可变;

来自this answer:

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

来自wiki:

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

我的问题是:

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

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

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

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

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

可以通过声明 n 来使持有人免受不当发布的影响 字段是最终的,这将使 Holder 不可变

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

如果满足以下条件,则对象是不可变的:
x 它的状态在之后不能被修改 建造;

x 它的所有字段都是最终的;[12] 和

x 它是正确的 构造(在构造过程中 this 引用不会转义)。

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

但是如果没有声明为 volatile,他们会在双重检查锁定模式中遭受Out-of-order writes 的影响吗?

【问题讨论】:

  • Java 内存模型保证安全发布,无需显式同步所有实例字段声明为 final 的不可变对象。
  • @scottb 不正确。构造函数本身可以泄漏引用。

标签: java multithreading immutability publish


【解决方案1】:

一个不可变的对象,例如String,似乎对所有读者都具有相同的状态,不管它的引用是如何获得的,即使是不正确的同步和缺乏发生前的关系。

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

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

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


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

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

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

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


这些是设计动机。现在让我们看看JLS是如何在jls-17.5.1中形式化它的

freeze 操作在构造函数出口处定义,与在最终字段的分配处相反。这允许我们在构造函数中的任何地方写入来填充内部状态。

不安全发布的常见问题是缺少happens-before (hb) 关系。即使读取看到写入,它也不会建立任何其他操作。但是,如果 volatile 读取看到 volatile 写入,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。

【讨论】:

  • 我在哪里可以找到关于 2 个部分订单的更多信息(比如一些书籍?):内存链 mc() 和取消引用链 dereferences()?我总是觉得 JLS 很难理解。谢谢!
  • 我不知道。如果作者写了一本关于mc() 的书,他不会出售任何副本来收回咖啡费用。最后,这些形式化并不重要。我们遵循常见的使用模式和经验法则。该语言就是为此而设计的。该规范是为实施者编写的。
【解决方案2】:

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

如果构造函数尝试注册新对象以进行回调(例如某些构造函数参数上的事件侦听器)或注册新对象,或者更巧妙地调用非最终方法,则引用泄漏的两种可能途径那被覆盖做同样的事情。

【讨论】:

  • 我读到了 JCiP 中的“this reference escaping”部分。我的问题是:除了构造函数内部的泄漏之外,还有其他方法可以使“此引用”逃脱吗?例如,“双重检查锁定”问题是否算作“此引用转义”的示例?
  • @du369 双重检查锁定是一个完全不同的结构。对泄漏的非构造引用的唯一方法是从构造函数直接(传递或分配this)或间接(调用本身可以访问this 的方法并泄漏它)。
  • 从构造函数中泄漏this 并不是不可变对象所特有或独有的问题。任何获得对未完全构造实例的引用的对象都可以在不一致或无效状态下查看该实例,无论是否可变。假设this 没有泄漏,Java 内存模型支持对真正不可变对象进行安全发布的假设。
  • @scottb 不,这不是不可变对象所独有的,但它是可以看到不可变对象处于不一致状态的唯一方式。
猜你喜欢
  • 1970-01-01
  • 2021-06-02
  • 1970-01-01
  • 1970-01-01
  • 2017-03-06
  • 1970-01-01
  • 2018-05-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多