有效不可变对象

14
我希望确保我正确理解Java内存模型中“有效不可变对象”的行为。假设我们有一个可变类,我们想将其发布为有效不可变对象:
class Outworld {
  // This MAY be accessed by multiple threads
  public static volatile MutableLong published;
}

// This class is mutable
class MutableLong {
  private long value;

  public MutableLong(long value) {
    this.value = value;
  }

  public void increment() {
    value++;
  }

  public long get() {
    return value;
  }
}

我们做以下事情:
// Create a mutable object and modify it
MutableLong val = new MutableLong(1);
val.increment();
val.increment();
// No more modifications
// UPDATED: Let's say for this example we are completely sure
//          that no one will ever call increment() since now

// Publish it safely and consider Effectively Immutable
Outworld.published = val;

问题是: Java内存模型是否保证所有线程都必须具有Outworld.published.get() == 3

根据Java并发实践,这应该是正确的,但如果我错了,请纠正我。

3.5.3. 安全发布惯用语

为了安全地发布对象,必须同时将对象的引用和对象状态对其他线程可见。可以通过以下方式安全地构造对象:
- 从静态初始化器初始化对象引用;
- 将引用存储到volatile字段或AtomicReference中;
- 将引用存储到正确构造的对象的final字段中;或
- 将引用存储到由锁正确保护的字段中。

3.5.4. 有效不可变对象

安全发布的有效不可变对象可以被任何线程安全地使用,无需额外同步。


请展示在对象引用可见之前建立对象状态的静态初始化程序 - trashgod
3个回答

9

是的。对 MutableLong 进行写操作后,在读取前会有一个 happens-before 关系(在 volatile 上)。

(有可能一个线程不安全地读取了 Outworld.published 并将其传递给另一个线程。理论上,这可能会看到早期状态。但实际上,我没有看到它发生过。)


2
你的评论(第二段)似乎与你的回答(第一段)不兼容,并且发生在易失性读取之前。你能详细说明一下吗? - assylias
5
假设线程T1安全地向线程T2发布消息。从T1到T2存在一个“happens-before”关系。但是,如果相同的对象从T2不安全地发布到T3,则从T2到T3没有“happens-before”关系,因此也没有从T1到T3的“happens-before”关系。 - Tom Hawtin - tackline
1
也有可能一个线程读取了 Outworld.published 并得到了 null - Holger
Tom Hawtin - tackline,如果假设MutableLong在构造后不会被修改,那么T2如何不安全地发布到T3? - RoK

5
Java内存模型必须满足一些条件才能保证Outworld.published.get() == 3
  • 您发布的代码片段创建并递增MutableLong,然后设置Outworld.published字段,这个过程必须在步骤之间具有可见性。一个简单的方法是将所有代码运行在单个线程中,从而保证"as-if-serial semantics"。我认为这就是您想要的,但值得指出。
  • Outworld.published的读取必须从赋值操作之后具有happens-after语义。例如,同一线程执行Outworld.published = val; 然后启动其他线程来读取该值。这将保证"as if serial"语义,防止在赋值之前重新排序读取操作。

如果您能够提供这些保证,那么JMM将保证所有线程都看到Outworld.published.get() == 3


然而,如果您对此领域的一般程序设计建议感兴趣,请继续阅读。

要确保没有其他线程永远看到不同的Outworld.published.get()值,您(开发人员)必须保证程序不以任何方式修改该值。无论是随后执行Outworld.published = differentVal;还是Outworld.published.increment();。虽然这是可以保证的,但如果您设计代码以避免可变对象和使用静态非final字段作为多个线程的全局访问点,则会更加容易:

  • 不要发布MutableLong,而是将相关值复制到不同类的新实例中,该实例的状态无法修改。例如:引入类ImmutableLong,它在构造时将value分配给final字段,并且没有increment()方法。
  • 不要让多个线程访问静态非final字段,而是将对象作为参数传递给您的Callable/Runnable实现。这将防止一个流氓线程重新分配值并干扰其他线程,并且比静态字段重新分配更容易理解。(诚然,如果您正在处理旧代码,则说起来容易做起来难)。

我完全同意你所说的。同步太容易出错了,我会尽量避免使用它。共享可变性不是设计算法或程序的安全方式。我认为每个程序员都应该花些时间研究函数式编程概念,并将其应用于面向对象的世界中。这绝对是值得的。 - bennidi

3
问题是:Java内存模型是否保证所有线程必须具有Outworld.published.get() == 3?
简短的答案是不。因为其他线程可能在读取之前访问Outworld.published。
在执行Outworld.published = val;之后的时刻,在没有对val进行其他修改的情况下,它始终为3。
但是,如果任何线程执行val.increment,则其值可能对其他线程不同。

是的,我在谈论的情况是当我们可以将这个对象视为“有效不可变”;也就是说,我们完全确定没有其他线程会在其上调用increment()。 我更新了一个例子以使其更具体。 - Volodymyr Sorokin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接