有效的不可变对象

我想确保我根据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. 安全出版习语

若要安全地发布对象,必须同时使对该对象的引用和该对象的状态对其他线程可见。正确构造的对象可以通过以下方式安全发布:
- 从静态初始值设定项初始化对象引用;
- 将对它的引用存储到易失性字段或原子引用中;
- 将对它的引用存储到正确构造的对象的最终字段中;or
- 将对它的引用存储到由锁正确保护的字段中。

3.5.4. 有效不可变的对象

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


答案 1

是的。上的写入操作后跟一个关系(在易失性上),然后再读取。MutableLonghappens-before

(一个线程可能以不安全的方式读取并将其传递给另一个线程。从理论上讲,这可以看到更早的状态。在实践中,我没有看到它发生。Outworld.published


答案 2

Java 内存模型必须满足几个条件才能保证:Outworld.published.get() == 3

  • 您发布的代码段创建并递增 ,然后设置字段,必须在步骤之间具有可见性。实现这一点的一种方法是让所有代码在单个线程中运行 - 保证“as-if-serial semantics”。我想这就是你的本意,但认为值得指出。MutableLongOutworld.published
  • 从赋值中读取必须具有发生后语义。这方面的一个例子可能是让同一线程执行,然后启动其他可以读取该值的线程。这将保证“好像串行”语义,防止在分配之前对读取进行重新排序。Outworld.publishedOutworld.published = val;

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


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

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

  • 而不是发布 ,而是将相关值复制到无法修改其状态的不同类的新实例中。例如:引入类,它在构造时分配给字段,并且没有方法。MutableLongImmutableLongvaluefinalincrement()
  • 而不是多个线程访问静态非最终字段,将对象作为参数传递给您的 / 实现。这将防止一个恶意线程重新分配值并干扰其他无管理系统线程的可能性,并且比静态字段重新分配更容易推理。(诚然,如果您正在处理遗留代码,这说起来容易做起来难)。CallableRunnable

推荐