不稳定的保证和无序执行

2022-09-01 02:10:31

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

请注意,这个问题完全是关于关键字和保证的:它不是关于关键字的(所以请不要回答“你必须使用synce”,因为我没有任何问题要解决:我只是想了解关于无序执行的保证(或缺乏保证)。volatilevolatilesynchronizedvolatile

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

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

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”?

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

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


答案 1

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

a = one;
b = two;

上面的语句不会重新排序,并且所有线程都将观察值,如果已经具有值。oneabtwo

这是大卫·福尔摩斯解释的一个帖子:
http://markmail.org/message/j7omtqqh6ypwshfv#query:+page:1+mid:34dnnukruu23ywzy+state:results

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

但是,如果一个人从另一个线程调用doIt()而不是调用setBothNonNull(...)的线程呢?

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


答案 2

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

假设分配给 和/或非 null 的值,我认为您的理解不正确。JLS是这样说的:ab

(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) 对易失性变量 (§8.3.1.4) 的写入与任何线程的所有后续读取同步(其中后续读取根据同步顺序定义)。vv

定理

假设线程 #1 调用了一次,并且参数为非 null,并且线程 #2 已观察到为非 null,则线程 #2 不能观察到是否为 null。setBoth(...);ba

非正式证明

  1. By (1) - hb(write(a, non-null), write(b, non-null)) in thread #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. By (4) - hb(write(a, non-null), read(b, non-null))
  5. By (4) - hb(write(a, non-null), read(a, XXX))

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

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


推荐