没有 volatile 的惰性初始化/记忆
Lazy initialization / memoization without volatile
看来Java内存模型没有定义本地缓存的"refreshing"和"flushing",人们只是为了简单起见才这样称呼它,但实际上"happens-before" 关系意味着以某种方式刷新和冲洗(如果你能解释这一点会很棒,但不是问题的直接部分)。
这让我很困惑,因为关于 Java Memory Model in the JLS 的部分没有以易于理解的方式编写。
因此,您能否告诉我我在以下代码中所做的假设是否正确,是否因此保证 运行 正确?
它部分基于 Double-checked locking 上维基百科文章中提供的代码,但是作者使用了包装器 class (FinalWrapper
),但这样做的原因是对我来说不是很明显。也许支持 null
值?
public class Memoized<T> {
private T value;
private volatile boolean _volatile;
private final Supplier<T> supplier;
public Memoized(Supplier<T> supplier) {
this.supplier = supplier;
}
public T get() {
/* Apparently have to use local variable here, otherwise return might use older value
* see https://jeremymanson.blogspot.com/2008/12/benign-data-races-in-java.html
*/
T tempValue = value;
if (tempValue == null) {
// Refresh
if (_volatile);
tempValue = value;
if (tempValue == null) {
// Entering refreshes, or have to use `if (_volatile)` again?
synchronized (this) {
tempValue = value;
if (tempValue == null) {
value = tempValue = supplier.get();
}
/*
* Exit should flush changes
* "Flushing" does not actually exists, maybe have to use
* `_volatile = true` instead to establish happens-before?
*/
}
}
}
return tempValue;
}
}
我还读到构造函数调用可以内联和重新排序,从而导致引用未初始化的 object(请参阅 this comment on a blog)。那么直接分配供应商的结果是否安全,还是必须分两步完成?
value = tempValue = supplier.get();
两步:
tempValue = supplier.get();
// Reorder barrier, maybe not needed?
if (_volatile);
value = tempValue;
编辑:这个问题的标题有点误导,目的是减少 volatile 字段的使用。如果初始化值已经在某个线程的缓存中,那么value
直接访问,不需要再去主存中查找
如果你只有几个单例,你可以减少 volatile 的使用。注意:您必须为每个单例重复此代码。
enum LazyX {
;
static volatile Supplier<X> xSupplier; // set somewhere before use
static class Holder {
static final X x = xSupplier.get();
}
public static X get() {
return Holder.x;
}
}
如果你认识供应商,这就变得简单了
enum LazyXpensive {
;
// called only once in a thread safe manner
static final Xpensive x = new Xpensive();
// after class initialisation, this is a non volatile read
public static Xpensive get() {
return x;
}
}
您可以通过使用 Unsafe
来避免使字段变得不稳定
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.function.Supplier;
public class LazyHolder<T> {
static final Unsafe unsafe = getUnsafe();
static final long valueOffset = getValueOffset();
Supplier<T> supplier;
T value;
public T get() {
T value = this.value;
if (value != null) return value;
return getOrCreate();
}
private T getOrCreate() {
T value;
value = (T) unsafe.getObjectVolatile(this, valueOffset);
if (value != null) return value;
synchronized (this) {
value = this.value;
if (value != null) return value;
this.value = supplier.get();
supplier = null;
return this.value;
}
}
public static Unsafe getUnsafe() {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
private static long getValueOffset() {
try {
return unsafe.objectFieldOffset(LazyHolder.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
throw new AssertionError(e);
}
}
}
但是,进行额外的查找是一种微观优化。如果您愿意为每个线程执行一次同步命中,则完全可以避免使用 volatile。
您的代码不是线程安全的,可以通过剥离所有不相关的部分轻松显示:
public class Memoized<T> {
private T value;
// irrelevant parts omitted
public T get() {
T tempValue = value;
if (tempValue == null) {
// irrelevant parts omitted
}
return tempValue;
}
}
因此 value
没有 volatile
修饰符,您正在 get()
方法中读取它而没有同步,当非 null
时,继续使用它而没有任何同步。
无论您在分配 value
时做什么,仅此代码路径就已经导致代码损坏,因为所有线程安全构造都需要两端(读取和写入端)使用兼容的同步机制.
你使用像 if (_volatile);
这样的深奥结构的事实变得无关紧要,因为代码已经被破坏了。
维基百科示例使用带有 final
字段的包装器的原因是仅使用 final
字段的不可变对象不受数据竞争的影响,因此,唯一在读取时是安全的构造它的引用没有同步操作。
请注意,由于 lambda 表达式属于同一类别,您可以使用它们来简化您的用例示例:
public class Memoized<T> {
private boolean initialized;
private Supplier<T> supplier;
public Memoized(Supplier<T> supplier) {
this.supplier = () -> {
synchronized(this) {
if(!initialized) {
T value = supplier.get();
this.supplier = () -> value;
initialized = true;
}
}
return this.supplier.get();
};
}
public T get() {
return supplier.get();
}
}
这里,Memoized.get()
中的supplier.get()
可能会在没有同步操作的情况下读取supplier
的更新值,在这种情况下它将读取正确的value
,因为它是隐式 final
。如果该方法读取 supplier
引用的过时值,它将在 synchronized(this)
块结束,该块使用 initialized
标志来确定是否需要对原始供应商进行评估。
由于 initialized
字段只能在 synchronized(this)
块中访问,因此它的计算结果总是正确的。每个线程最多执行一次此块,而只有第一个线程会在原始供应商上评估 get()
。之后,每个线程将使用 () -> value
供应商,返回值而不需要任何同步操作。
看来Java内存模型没有定义本地缓存的"refreshing"和"flushing",人们只是为了简单起见才这样称呼它,但实际上"happens-before" 关系意味着以某种方式刷新和冲洗(如果你能解释这一点会很棒,但不是问题的直接部分)。
这让我很困惑,因为关于 Java Memory Model in the JLS 的部分没有以易于理解的方式编写。
因此,您能否告诉我我在以下代码中所做的假设是否正确,是否因此保证 运行 正确?
它部分基于 Double-checked locking 上维基百科文章中提供的代码,但是作者使用了包装器 class (FinalWrapper
),但这样做的原因是对我来说不是很明显。也许支持 null
值?
public class Memoized<T> {
private T value;
private volatile boolean _volatile;
private final Supplier<T> supplier;
public Memoized(Supplier<T> supplier) {
this.supplier = supplier;
}
public T get() {
/* Apparently have to use local variable here, otherwise return might use older value
* see https://jeremymanson.blogspot.com/2008/12/benign-data-races-in-java.html
*/
T tempValue = value;
if (tempValue == null) {
// Refresh
if (_volatile);
tempValue = value;
if (tempValue == null) {
// Entering refreshes, or have to use `if (_volatile)` again?
synchronized (this) {
tempValue = value;
if (tempValue == null) {
value = tempValue = supplier.get();
}
/*
* Exit should flush changes
* "Flushing" does not actually exists, maybe have to use
* `_volatile = true` instead to establish happens-before?
*/
}
}
}
return tempValue;
}
}
我还读到构造函数调用可以内联和重新排序,从而导致引用未初始化的 object(请参阅 this comment on a blog)。那么直接分配供应商的结果是否安全,还是必须分两步完成?
value = tempValue = supplier.get();
两步:
tempValue = supplier.get();
// Reorder barrier, maybe not needed?
if (_volatile);
value = tempValue;
编辑:这个问题的标题有点误导,目的是减少 volatile 字段的使用。如果初始化值已经在某个线程的缓存中,那么value
直接访问,不需要再去主存中查找
如果你只有几个单例,你可以减少 volatile 的使用。注意:您必须为每个单例重复此代码。
enum LazyX {
;
static volatile Supplier<X> xSupplier; // set somewhere before use
static class Holder {
static final X x = xSupplier.get();
}
public static X get() {
return Holder.x;
}
}
如果你认识供应商,这就变得简单了
enum LazyXpensive {
;
// called only once in a thread safe manner
static final Xpensive x = new Xpensive();
// after class initialisation, this is a non volatile read
public static Xpensive get() {
return x;
}
}
您可以通过使用 Unsafe
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.function.Supplier;
public class LazyHolder<T> {
static final Unsafe unsafe = getUnsafe();
static final long valueOffset = getValueOffset();
Supplier<T> supplier;
T value;
public T get() {
T value = this.value;
if (value != null) return value;
return getOrCreate();
}
private T getOrCreate() {
T value;
value = (T) unsafe.getObjectVolatile(this, valueOffset);
if (value != null) return value;
synchronized (this) {
value = this.value;
if (value != null) return value;
this.value = supplier.get();
supplier = null;
return this.value;
}
}
public static Unsafe getUnsafe() {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
private static long getValueOffset() {
try {
return unsafe.objectFieldOffset(LazyHolder.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
throw new AssertionError(e);
}
}
}
但是,进行额外的查找是一种微观优化。如果您愿意为每个线程执行一次同步命中,则完全可以避免使用 volatile。
您的代码不是线程安全的,可以通过剥离所有不相关的部分轻松显示:
public class Memoized<T> {
private T value;
// irrelevant parts omitted
public T get() {
T tempValue = value;
if (tempValue == null) {
// irrelevant parts omitted
}
return tempValue;
}
}
因此 value
没有 volatile
修饰符,您正在 get()
方法中读取它而没有同步,当非 null
时,继续使用它而没有任何同步。
无论您在分配 value
时做什么,仅此代码路径就已经导致代码损坏,因为所有线程安全构造都需要两端(读取和写入端)使用兼容的同步机制.
你使用像 if (_volatile);
这样的深奥结构的事实变得无关紧要,因为代码已经被破坏了。
维基百科示例使用带有 final
字段的包装器的原因是仅使用 final
字段的不可变对象不受数据竞争的影响,因此,唯一在读取时是安全的构造它的引用没有同步操作。
请注意,由于 lambda 表达式属于同一类别,您可以使用它们来简化您的用例示例:
public class Memoized<T> {
private boolean initialized;
private Supplier<T> supplier;
public Memoized(Supplier<T> supplier) {
this.supplier = () -> {
synchronized(this) {
if(!initialized) {
T value = supplier.get();
this.supplier = () -> value;
initialized = true;
}
}
return this.supplier.get();
};
}
public T get() {
return supplier.get();
}
}
这里,Memoized.get()
中的supplier.get()
可能会在没有同步操作的情况下读取supplier
的更新值,在这种情况下它将读取正确的value
,因为它是隐式 final
。如果该方法读取 supplier
引用的过时值,它将在 synchronized(this)
块结束,该块使用 initialized
标志来确定是否需要对原始供应商进行评估。
由于 initialized
字段只能在 synchronized(this)
块中访问,因此它的计算结果总是正确的。每个线程最多执行一次此块,而只有第一个线程会在原始供应商上评估 get()
。之后,每个线程将使用 () -> value
供应商,返回值而不需要任何同步操作。