Java 内存模型和并发读取


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

幸运的是,有两个限制可以完成这项工作:

  1. c.c = null在所有其他操作之前发生(参见 JLS 17.4.5(。
  2. 从给定线程的角度来看,在该线程上发生的操作始终以它们在代码中出现的顺序发生(也是 JLS 17.4.5(。所以对于线程 1,o1 = c.o发生在o2 = c.o之前。(线程2 不必按该顺序查看这些读取...但它根本看不到o1o2,所以那里没有问题。

其中第一个意味着我们不能采取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来消除数据竞争将解决此问题。

最新更新