有效不可变对象

时间:2012-04-20 22:41:23

标签: java concurrency immutability java-memory-model

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

     

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

     

3.5.4。有效不可变的对象

     

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

3 个答案:

答案 0 :(得分:5)

是。在MutableLong之后,写入操作之后是happens-before关系(在volatile上)。

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

答案 1 :(得分:4)

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()方法。
  • 而不是多个线程访问静态非final字段,将对象作为参数传递给Callable / Runnable实现。这将防止一个流氓线程重新分配值并干扰其他线程的可能性,并且比静态字段重新分配更容易推理。 (诚​​然,如果你正在处理遗留代码,说起来容易做起来难。)

答案 2 :(得分:2)

  

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

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

在执行Outworld.published = val;之后,在val没有进行任何其他修改的情况下 - 是 - 它始终为3

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