class C {
Object o;
public void set(Object o){
if(this.o == null){
this.o = o;
}
}
public Object get(){
return o;
}
}
C c = new C();
C c = new C();
Thread#1
Object o1 = c.get(); // 1
Object o2 = c.get(); // 2
Thread#2
c.set(new Object());
有没有可能o2 == null && o1 != null
? 为什么?
为了明确我的意思,我编辑了:
如果我们遇到以下情况怎么办:
C c = new C(); // it is given at beginning
1) Object o2 = c.o; // o2 is null. This operation was **reordered** before O `o1 = c.o. The JVM can do it because JMM allows do it.
2) c.o = new Object()` //Thread #2 was executed
3) O o1 = c.o // o1 is not null while o2 is.
这是不可能的,尽管你有一个数据竞赛。
数据竞争是因为围绕o
的获取和设置不同步,这意味着它们没有先于顺序发生。你可以通过synchronized
这两种方法,或者通过使o
易失性,或者通过其他几种方式来解决这个问题。
如果没有同步,JVM 可以对其他线程看到的事件重新排序。从 Thread1 的角度来看,您有以下事件(为简单起见,使用内联的方法(:
c.o = null; // initial value
o1 = c.o;
o2 = c.o;
c.o = new Object(); // from Thread2
幸运的是,有两个限制可以完成这项工作:
c.c = null
在所有其他操作之前发生(参见 JLS 17.4.5(。- 从给定线程的角度来看,在该线程上发生的操作始终以它们在代码中出现的顺序发生(也是 JLS 17.4.5(。所以对于线程 1,
o1 = c.o
发生在o2 = c.o
之前。(线程2 不必按该顺序查看这些读取...但它根本看不到o1
或o2
,所以那里没有问题。
其中第一个意味着我们不能采取c.o = null
行动并在c.c = new Object()
后订购。第二个意味着从 Thread1 的角度来看,您在帖子底部提到的重新排序是不允许的(当然,Thread1 是唯一看到有关 o1 或 o2 的任何内容的线程(。
结合这两个限制,我们可以看到,如果 Thread1 看到c.o
为非空,那么它将永远不会看到它再次恢复为 null。如果 o1 不为空,则 o2 也必须为空。
请注意,这是非常善变的。例如,假设 Thread2 不是只设置c.o
一次,而是设置两次:
c.set("one");
c.set("two");
在这种情况下,可以看到o1
是"二",而o2
是"一"。这是因为那里的操作是:
c.o = null; // initial value
o1 = c.o;
o2 = c.o;
c.c = "one"; // from Thread2
c.c = "two"; // from Thread2
JVM 可以按照它认为合适的方式对 Thread2 中的项目进行重新排序,只要它们不出现在该c.c = null
之前。特别是,这是有效的:
c.o = null; // initial value
c.c = "two"; // from Thread2
o1 = c.o;
c.c = "one"; // from Thread2
o2 = c.o;
通过将获取和设置为同步到o
来消除数据竞争将解决此问题。