我在阅读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
旁边有一条评论 field :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)
据我所知, volatile 读取将“刷新” volatile 读取后读取的变量值。换句话说 - 它耗尽了缓存的失效队列(如果我们谈论 MESI 一致性协议(protocol))。此外,它可以防止在 volatile 读取之后发生的加载/读取在它之前被重新排序。尽管如此,它并不保证对象创建中的指令(调用供应商 get()
方法)不会被重新排序。我的问题是 - 为什么这段代码被认为是线程安全的?
显然,我在评论中的来源中没有发现任何有趣的东西。
最佳答案
当谈到 Java 的内存模型时,不要谈论缓存。重要的是正式的先发生于关系。
请注意 computeValue()
已声明 synchronized
,所以对于执行该方法的线程来说,方法内的重新排序是无关紧要的,因为它们只能在之前执行该方法的任何线程已经退出该方法时才能进入该方法,并且前一个方法退出之间存在happens-before关系线程和进入方法的下一个线程。
真正有趣的方法是
public T get() {
return (supplier == null) ? value : computeValue();
}
不使用 synchronized
但依赖于 volatile
阅读 supplier
.这显然是假设 supplier
的初始状态是非 null
,例如在构造函数和周围代码中分配确保get
在此分配发生之前,方法无法执行。然后,当
supplier
读作 null
,只能是写的结果,第一个线程执行computeValue()
已完成,这在线程在分配 null
之前所做的写入之间建立了一个发生在之前的关系。至 supplier
以及该线程在阅读后所做的阅读 null
来自 supplier
.所以它会感知到 value
引用的对象的完全初始化状态。 .所以你是对的,值的构造函数中发生的事情可以通过
value
的赋值重新排序。引用,但它们不能在随后写入 supplier
时重新排序,其中 get
方法是靠。
关于java - volatile 变量读取行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69034955/