13

我想确保根据 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 Concurrency In Practice,这应该是正确的,但如果我错了,请纠正我。

3.5.3. 安全出版习语

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

3.5.4。有效的不可变对象

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

4

3 回答 3

9

是的。在读取之前,对 的写入操作MutableLong后跟一个happens-before关系(在 volatile 上)。

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

于 2012-04-20T22:51:05.897 回答
5

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

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

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


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

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

  • 而不是发布MutableLong,将相关值复制到不同类的新实例中,其状态无法修改。例如:引入类ImmutableLong,它在构造时分配value给一个final字段,并且没有increment()方法。
  • 而不是多个线程访问静态非最终字段,将对象作为参数传递给您的Callable/Runnable实现。这将防止一个流氓线程重新分配值并干扰其他线程的可能性,并且比静态字段重新分配更容易推理。(诚​​然,如果您正在处理遗留代码,这说起来容易做起来难)。
于 2012-04-21T11:09:49.170 回答
3

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

简短的回答是no。因为其他线程可能会Outworld.published在它被读取之前访问。

在执行的那一刻之后Outworld.published = val;,在没有其他修改的情况下val- 是的 - 它总是3

但是,如果任何线程执行val.increment,那么它的值对于其他线程可能会有所不同。

于 2012-04-21T03:39:20.610 回答