我们有以下课程:
public class MyClass {
private final List<String> myList = new ArrayList<>(); //Not a thread-safe thing
// Called on thread 1
MyClass() {
myList.add("foo");
}
// Called on thread 2
void add(String data) {
myList.add(data);
}
}
它的格式是否良好?
我只能找到这个:
当对象的构造函数完成时,就认为该对象已完全初始化。仅在对象完全初始化后才能看到对该对象的引用的线程保证看到该对象的最终字段的正确初始化值。
https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5
这意味着“线程 2”必须看到
ArrayList
,但可能会或可能不会看到其内容,对吧?
也就是说,是否可以有以下顺序:
T1:创建
MyClass
以及 ArrayList
T2:访问
ArrayList
T1:将“foo”添加到列表中
你的想法是错误的。这是一个明智的想法,但是,JMM 的这一部分 (JMM §17.5):
它还将看到这些最终字段引用的任何对象或数组的版本,这些版本至少与最终字段一样最新。
不同意分析。因此,假设您遵守 JMM 该部分规定的规则,即:
不要让
this
引用从构造函数中逃逸。例如如果构造函数执行 someStaticField = this;
或 someOtherThing.hereYouGo(this);
,则任何使用该 this
引用的代码都无法获得保证。
那么不仅直接值是安全的(对于基元来说是值,对于对象来说是引用(即指针)),而且该对象的字段/该数组的槽也是安全的。这里的“安全”意味着:引用它的任何代码(除了通过上面的异常,您没有获得这种安全性)都无法观察到构造函数
完成之前的状态中的任何
final
字段。
因此,您的代码片段格式良好。
当然请注意,为构造函数调用赋值的行为本身并不包含在其中。因此:
class Example {
MyObject c;
void codeInThread1() {
c = new MyObject();
}
void codeInThread2() {
System.out.println(c);
}
}
可能会导致线程 2 打印
null
(因为 c
被线程 2 观察为 null
),即使代码有其他效果,清楚地表明线程 1 已经远远超过了 c = new MyObject();
。 JMM 不保证一个线程的写入被另一个线程观察到,除非建立了 Happens-Before 关系。
但是,JVM 确实保证不会对对象引用进行剪切写入,并保证一定的一致性:线程 2 只能观察到两件事。要么是
null
,要么是完全初始化的对象,即没有原来的状态 before 构造函数完成之前可以通过线程 2 观察到。
从技术上讲,上述内容有点不正确。 JMM 表示“至少与最终字段一样最新”。这意味着对最终字段的实际写入是“锁定”的行为 - JMM 保证您可以从此引用获取的任何状态“至少是最新的”。然而,在你的片段中,那个时刻“太早”了。所以,这是完成同样事情的技术上正确的方法:
public class MyClass {
private final List<String> myList;
// Called on thread 1
MyClass() {
var list = new ArrayList<>();
list.add("foo");
myList = list; // lock it in!
}
// Called on thread 2
void add(String data) {
myList.add(data);
}
}
但是,我很确定不存在 JVM+OS+架构的组合,您可以在“myList 指向空数组列表”状态下观察该对象。然而,JMM 并不需要 JVM+OS+arch 组合来消除该选项。上面的代码确实消除了它(例如,让您观察空列表的 JVM+OS+arch 组合已损坏,您应该提交错误 - 因为它违反了 §17.5 的引用条款)。