易失性变量读取行为
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
方法依赖于。
我在阅读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
方法依赖于。