我想确保根据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。有效不可变的对象
安全发布有效的不可变对象可以安全使用 没有额外同步的任何线程。
答案 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()
方法。Callable
/ Runnable
实现。这将防止一个流氓线程重新分配值并干扰其他线程的可能性,并且比静态字段重新分配更容易推理。 (诚然,如果你正在处理遗留代码,说起来容易做起来难。)答案 2 :(得分:2)
问题是:Java内存模型是否保证所有线程 必须有Outworld.published.get()== 3?
简短回答是no
。因为其他线程可能在读取之前访问Outworld.published
。
在执行Outworld.published = val;
之后,在val
没有进行任何其他修改的情况下 - 是 - 它始终为3
。
但是如果任何线程执行val.increment
,那么其他线程的值可能会有所不同。