有效不可变对象
我想确保根据Java Memory Model正确理解’Effectively Immutable Objects’行为。
假设我们有一个可变类,我们希望将其发布为有效的不可变类:
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。 有效不可变的对象
安全发布的有效不可变对象可以被任何线程安全地使用而无需额外的同步。
是。 MutableLong
上的写操作之后是读之前的happens-before
关系(在volatile上)。
(有可能一个线程读取Outworld.published
并将其不安全地传递给另一个线程。理论上,这可以看到更早的状态。在实践中,我看不到它发生。)
Java内存模型必须满足几个条件才能保证Outworld.published.get() == 3
:
- 您发布的代码片段创建并增加
MutableLong
,然后设置Outworld.published
字段, 必须在步骤之间显示可见性 。 实现这一目标的一种方法是让所有代码在单个线程中运行 – 保证“ as-if-serial语义 ”。 我认为这是你的意图,但认为值得指出。 - 读取
Outworld.published
必须在赋值语义之后发生 。 一个例子就是让同一个线程执行Outworld.published = val;
然后启动其他可以读取值的线程。 这将保证“ 就像串行 ”语义,防止在赋值之前重新排序读取。
如果您能够提供这些保证,那么JMM将保证所有线程都能看到Outworld.published.get() == 3
。
但是,如果您对该领域的一般项目设计建议感兴趣,请继续阅读。
为了保证没有其他线程看到Outworld.published.get()
的不同值,您(开发人员)必须保证您的程序不会以任何方式修改该值。 随后执行Outworld.published = differentVal;
或者Outworld.published.increment();
。 虽然可以保证,但如果您设计代码以避免可变对象,并使用静态非最终字段作为多个线程的全局访问点,则可以更容易:
- 而不是发布
MutableLong
,将相关值复制到不同类的新实例,其状态不能被修改。 例如:引入类ImmutableLong
,它在构造时为final
字段value
,并且没有increment()
方法。 - 而不是多个线程访问静态非最终字段,将对象作为参数传递给
Callable
/Runnable
实现。 这将防止一个流氓线程重新分配值并干扰其他线程的可能性,并且比静态字段重新分配更容易推理。 (诚然,如果你正在处理遗留代码,说起来容易做起来难)。
问题是:Java内存模型是否保证所有线程都必须具有Outworld.published.get()== 3?
简短的回答是no
。 因为其他线程可能在读取之前访问Outworld.published
。
在Outworld.published = val;
之后的那一刻Outworld.published = val;
已经完成,条件是没有其他修改与val
– 是 – 它总是3
。
但是如果任何线程执行val.increment
那么其值对于其他线程可能会有所不同。