你的 idea 是错误的.这是一个明智的思路,但是,JMM(JMM §17.5)的这一节:
它还将看到那些最终字段引用的任何对象或数组的版本,这些对象或数组至少与最终字段一样最新.
不同意这种分析.因此,假设您遵守JMM中那一节规定的规则,即:
Do not let the 100 ref escape from the constructor. e.g. if the constructor does 101 or 102, any code that uses that 100 reference does not get the guarantee.
那么,不仅直接值是安全的(对于原语来说,值是安全的,对于对象来说,是引用(即指针)),而且对象的字段/数组的槽也是安全的."安全"在这里的意思是:任何引用它的代码(除了通过上面的异常,您不能获得这种安全性)都不能像构造函数finishes之前那样观察处于状态的final
个字段中的任何一个.
因此,您的代码片段格式良好.
当然,请注意,为构造函数调用赋值的行为本身并不包含在本文中.因此:
class Example {
MyObject c;
void codeInThread1() {
c = new MyObject();
}
void codeInThread2() {
System.out.println(c);
}
}
可以导致线程2打印null
(因为线程2将c
观察为null
)、even if代码已经清楚地指示线程1已经进展到远远超过c = new MyObject();
的其他效果.JMM不保证一个线程的写入被另一个线程观察到,除非建立关系之前发生.
然而,JVM确实保证不会对对象引用和一定的一致性进行剪切写入:线程2只能观察到两件事.线程2可以观察到null
或完全初始化的对象,即没有构造函数完成时的状态.
等一等!
编辑:不,不要坚持--你可以走了.请参见此问题的另一个答案,以解释为什么您的代码片段完全正确,并且无需担心对象after的Mutations 会将其分配给最终的字段.
从技术上讲,上面的说法几乎是不正确的.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+体系 struct 的组合,您可以在‘myList指向空数组列表’的状态下观察到该对象.然而,JMM并没有采用JVM+OS+ARCH组合来消除该选项.上面的代码does消除了它(就像在中一样,允许您观察空列表的JVM+OS+ARCH组合被 destruct 了,您应该提交一个错误-因为它违反了§17.5的引号条款).