易失性保证和无序执行

时间:2010-03-14 05:09:34

标签: java volatile java-memory-model

重要编辑我知道发生两项任务的线程中的“发生之前” 我的问题是是否可能 em>线程读取“b”非空,而“a”仍为空。所以我知道如果你从与之前调用 setBothNonNull(...)的线程相同的线程中调用 doIt(),那么它就不能抛出NullPointerException。但是如果一个人从另一个线程调用 doIt() 而不是调用 setBothNonNull(...)的那个呢?

请注意,此问题仅涉及volatile关键字和volatile保证:关于synchronized关键字的(所以请不要回答“你必须使用同步”,因为我没有任何问题需要解决:我只是想了解关于无序执行的volatile保证(或缺乏保证)。

假设我们有一个包含两个volatile String引用的对象,它们被构造函数初始化为null,并且我们只有一种方法可以修改两个String:通过调用 setBoth(...)< / em>并且我们之后只能将它们的引用设置为非null引用(只允许构造函数将它们设置为null)。

例如(这只是一个例子,毫无疑问):

public class SO {

    private volatile String a;
    private volatile String b;

    public SO() {
        a = null;
        b = null;
    }

    public void setBothNonNull( @NotNull final String one, @NotNull final String two ) {
        a = one;
        b = two;
    }

    public String getA() {
        return a;
    }

    public String getB() {
        return b;
    }

}

setBothNoNull(...)中,指定非空参数“a”的行出现在指定非空参数“b”的行之前。

然后,如果我这样做(再一次,毫无疑问,接下来会出现问题):

doIt() {
    if ( so.getB() != null ) {
        System.out.println( so.getA().length );
    }
}

我的理解是正确的,由于无序执行,我可以得到 NullPointerException

换句话说:不能保证因为我读了一个非空的“b”我会读一个非空的“a”吗?

因为无序(多)处理器和volatile工作方式“b”可以在“a”之前分配?

volatile保证写入后的读取总是会看到最后写入的值,但是这里有一个无序的“问题”对吗? (再一次,“问题”是为了试图理解volatile关键字和Java内存模型的语义,而不是解决问题)。

5 个答案:

答案 0 :(得分:25)

不,你永远不会得到NPE。这是因为volatile也具有引入先发生关系的记忆效应。换句话说,它将阻止

的重新排序
a = one;
b = two;

如果one已经具有值a,则上述语句将不会重新排序,并且b的所有主题都会观察到值two

以下是David Holmes解释的一个主题:
http://markmail.org/message/j7omtqqh6ypwshfv#query:+page:1+mid:34dnnukruu23ywzy+state:results

EDIT(对后续行动的回应): Holmes所说的是,如果只有线程A,编译器可以在理论上进行重新排序。但是,还有其他线程,并且它们可以检测到重新排序。这就是不允许编译器进行重新排序的原因。 java内存模型要求编译器专门确保没有线程会检测到这种重新排序。

  

但是如果有人从中调用doIt()会怎么样呢   另一个线程比一个调用   setBothNonNull(...)?

不,你仍然永远不会有NPE。 volatile语义确实强加了线程间排序。这意味着,对于所有现有线程,one的分配在two的分配之前发生。

答案 1 :(得分:8)

  

我的理解是正确的,由于乱序执行,我可以获得NullPointerException吗?换句话说:不能保证因为我读了一个非空的“b”我会读一个非空的“a”吗?

假设分配给ab或非空的值,我认为您的理解不正确。 JLS说:

  

1 )如果x和y是同一个线程的动作,而x在程序顺序中位于y之前,那么hb(x,y)。

     

2 )如果某个动作x与后续动作y同步,那么我们也有hb(x,y)。

     

3 )如果是hb(x,y)和hb(y,z),那么hb(x,z)。

  

4 )对volatile变量(第8.3.1.4节)v的写入与任何线程的v 的所有后续读取同步(其中后续根据同步顺序定义)。

<强>定理

  

鉴于线程#1已调用setBoth(...);一次,并且参数为非null,并且线程#2已观察到b为非null,则线程#2不能将a视为空。

非正式证明

  1. 通过( 1 ) - 线程#1中的hb(写(a,非空),写(b,非空))
  2. 由( 2 )和( 4 ) - hb(写(b,非空),读(b,非空))
  3. 通过( 1 ) - hb(读取(b,非空),读取(a,XXX))在第2号线中,
  4. By( 4 ) - hb(写(a,非空),读(b,非空))
  5. By( 4 ) - hb(写(a,非空),读(a,XXX))
  6. 换句话说,将a的非空值写入“在读取a的值(XXX)之前”。 XXX可以为null的唯一方法是,如果有其他操作将null写入a,使得hb(写入(a,非空),写入(a,XXX))和hb(写入(a) ,XXX),阅读(a,XXX))。根据问题定义,这是不可能的,因此XXX不能为空。 QED。

    解释 - JLS声明hb(...)(“之前发生”)关系并不完全禁止重新排序。但是,如果hb(xx,yy),则如果结果代码具有与原始序列相同的可观察效果,​​则允许重新排序动作xx和yy仅

答案 2 :(得分:2)

我发现以下帖子解释了volatile在这种情况下具有与synchronized相同的排序语义。 Java Volatile is Powerful

答案 3 :(得分:2)

虽然斯蒂芬·C和公认的答案很好并且几乎涵盖了它,但值得注意的是,变量 a 并不是必须的。不稳定 - 你仍然没有获得NPE。 这是因为a = oneb = two之间会出现先前发生过的关系,无论avolatile。所以Stephen C的正式证据仍然适用,不需要a易变。

答案 4 :(得分:0)

我读了这个page,发现了一个非挥发性的&amp;您的问题的非同步版本:

class Simple {
    int a = 1, b = 2;
    void to() {
        a = 3;
        b = 4;
    }
    void fro() {
        System.out.println("a= " + a + ", b=" + b);
    }
}

fro可以为a的值获得1或3,并且b的值可以单独获得2或4。

(我意识到这并没有回答你的问题,但它补充了它。)