易失性变量读取行为

Mar*_*tin 2 java concurrency lazy-loading volatile vavr

我在阅读Vavr的Lazy源码时遇到过如下代码:

private transient volatile Supplier<? extends T> supplier;
private T value; // will behave as a volatile in reality, because a supplier volatile read will update all fields (see https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile)
Run Code Online (Sandbox Code Playgroud)
public T get() {
    return (supplier == null) ? value : computeValue();
}

private synchronized T computeValue() {
    final Supplier<? extends T> s = supplier;
    if (s != null) {
        value = s.get();
        supplier = null;
    }
    return value;
}
Run Code Online (Sandbox Code Playgroud)

这是一个著名的模式,称为“双重检查锁定”,但对我来说它看起来很糟糕。假设我们将此代码嵌入到多线程环境中。如果该get()方法由第一个线程调用并且供应商构造了一个新对象,(对我而言)由于以下代码的重新排序,另一个线程有可能看到半构造的对象:

private synchronized T computeValue() {
    final Supplier<? extends T> s = supplier;
    if (s != null) {
        // value = s.get(); suppose supplier = () -> new AnyClass(x, y , z)
        temp = calloc(sizeof(SomeClass));
        temp.<init>(x, y, z);
        value = temp; //this can be reordered with line above
        supplier = null;
    }
    return value;
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,该value字段旁边有一条评论:

private transient volatile Supplier<? extends T> supplier;
private T value; // will behave as a volatile in reality, because a supplier volatile read will update all fields (see https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile)
Run Code Online (Sandbox Code Playgroud)

据我所知,易失性读取将“刷新”易失性读取后读取的变量值。换句话说 - 它耗尽了缓存的失效队列(如果我们谈论 MESI 一致性协议)。此外,它可以防止在易失性读取之后发生的加载/读取在它之前被重新排序。尽管如此,它并不保证对象创建(调用供应商get()方法)中的指令不会被重新排序。

我的问题是 - 为什么这段代码被认为是线程安全的?

显然,我在评论中的来源中没有发现任何有趣的东西。

Hol*_*ger 5

当谈到 Java 的内存模型时,不要谈论缓存。重要的是正式的先发生于关系。

请注意,computeValue()声明为synchronized,因此对于执行该方法的线程而言,该方法内的重新排序是无关紧要的,因为它们只能在之前执行该方法的任何线程已经退出该方法并且该方法之间存在happens-before关系时才能进入该方法前一个线程退出,下一个线程进入该方法。

真正有趣的方法是

public T get() {
    return (supplier == null) ? value : computeValue();
}
Run Code Online (Sandbox Code Playgroud)

不使用synchronized,但依靠volatile的读取supplier。这显然是假设的初始状态supplier是 non- null,例如在构造函数中赋值,并且周围的代码确保该get方法在此赋值发生之前无法执行。

然后,当supplier读为null,那只能是写的结果,第一个线程执行computeValue()已经完成,这建立之前发生线程分配前进行的写操作之间的关系null,以supplier和阅读后读取该线程进行nullsupplier。所以它会感知到被引用的对象的完全初始化状态value

所以你是对的,值的构造函数中发生的事情可以通过value引用的分配重新排序,但不能通过随后的 write to 重新排序supplier,该get方法依赖于。