易失性变量读取行为

Volatile variable read behavior

我在阅读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)
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;
}

这是一个名为“双重检查锁定”的著名模式,但对我来说它看起来很糟糕。假设我们将这段代码嵌入到多线程环境中。如果第一个线程调用 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;
}

不幸的是,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)

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

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

显然,我在评论中的源代码中没有发现任何有趣的内容。

说到Java的内存模型就别谈缓存了。重要的是正式的 happens-before 关系。

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

真正有趣的方法是

public T get() {
    return (supplier == null) ? value : computeValue();
}

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

那么,当supplier读为null时,只能是写的结果,第一个执行computeValue()的线程已经done了,这就建立了一个happens-before 线程在将 null 分配给 supplier 之前进行的写入与此线程在从 supplier 读取 null 之后进行的读取之间的关系。所以它会感知到value.

引用的对象的一个​​完全初始化状态

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