42

重要编辑我知道两个分配发生的线程中的“之前发生”我的问题是另一个线程是否有可能在“a”仍然为空时读取“b”非空。所以我知道,如果你从与之前调用setBothNonNull(...)的线程相同的线程调用doIt() ,那么它不能抛出 NullPointerException。但是,如果从另一个线程调用doIt()而不是调用setBothNonNull(...)怎么办?

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

假设我们有一个包含两个volatileString 引用的对象,这些引用由构造函数初始化为 null,并且我们只有一种方法来修改这两个 String:通过调用setBoth(...)并且我们只能在之后将它们的引用设置为非空引用(仅允许构造函数将它们设置为空)。

例如(这只是一个例子,还没有问题):

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 内存模型的语义,而不是解决问题)。

4

5 回答 5

26

不,你永远不会得到 NPE。这是因为volatile还具有引入发生前关系的记忆效应。换句话说,它将防止重新排序

a = one;
b = two;

上面的语句不会被重新排序,所有线程都会观察 valueone是否已经有avalue 。btwo

这是 David Holmes 解释的线程:
http ://markmail.org/message/j7omtqqh6ypwshfv#query:+page:1+mid:34dnnukruu23ywzy+state:results

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

但是,如果从另一个线程调用 doIt() 而不是调用 setBothNonNull(...) 怎么办?

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

于 2010-03-14T06:39:33.610 回答
8

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

假设赋值给aandb或非null,我认为你的理解是不正确的。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(...);了一次,并且参数是非空的,并且线程#2 已经观察b到非空,那么线程#2 就不能观察a 到空。

非正式证明

  1. By ( 1 ) - hb(write(a, non-null), write(b, non-null)) 在线程 #1
  2. 通过 ( 2 ) 和 ( 4 ) - hb(write(b, non-null), read(b, non-null))
  3. 通过 ( 1 ) - hb(read(b, non-null), read(a, XXX)) 在线程 #2 中,
  4. 通过 ( 4 ) - hb(写(a, non-null), read(b, non-null))
  5. 通过 ( 4 ) - hb(写(a, 非空), 读(a, XXX))

换句话说,将非空值写入a“发生在”读取 的值 (XXX) 之前a。XXX 可以为 null 的唯一方法是,如果有其他操作写入 null,a例如 hb(write(a,non-null), write(a,XXX)) 和 hb(write(a,XXX), read (a,XXX))。根据问题定义这是不可能的,因此 XXX 不能为空。QED。

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

于 2010-03-14T06:00:03.563 回答
2

虽然斯蒂芬 C 和公认的答案很好并且几乎涵盖了它,但值得指出的是,变量a不一定是易变的 - 你仍然不会得到 NPE。这是因为 和 之间存在发生前的关系a = oneb = two无论是否avolatile。所以斯蒂芬 C 的形式证明仍然适用,只是不需要a易变。

于 2013-03-04T14:21:52.140 回答
2

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

于 2010-03-14T07:16:34.373 回答
0

我阅读了此页面并找到了您问题的非易失性和非同步版本:

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

fro可以得到 1 或 3 的值a,并且独立地可以得到 2 或 4 的值b

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

于 2010-03-14T06:05:32.560 回答