Mar*_*234 5 java memoization java-memory-model lazy-initialization
看起来Java内存模型没有定义本地缓存的“刷新”和“刷新”,相反,人们只是为了简单起见才这样称呼它,但实际上“ happens-before”关系意味着以某种方式刷新和刷新(如果您可以对此进行解释,但不能直接成为问题的一部分)。
令我感到困惑的是,JLS中有关Java内存模型的部分并未以易于理解的方式编写。
因此,能否请您告诉我我在以下代码中所做的假设是否正确以及是否可以保证正常运行?
它部分基于Wikipedia文章中有关双重检查锁定的代码,但是作者在其中使用了包装器类(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;
}
}
Run Code Online (Sandbox Code Playgroud)
我也读过,构造函数调用可以内联和重新排序,从而导致对未初始化对象的引用(请参阅Blog上的注释)。那么直接分配供应商的结果是否安全,还是必须分两步进行?
value = tempValue = supplier.get();
Run Code Online (Sandbox Code Playgroud)
两步:
tempValue = supplier.get();
// Reorder barrier, maybe not needed?
if (_volatile);
value = tempValue;
Run Code Online (Sandbox Code Playgroud)
编辑:这个问题的标题有点误导,目标是减少对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;
}
}
Run Code Online (Sandbox Code Playgroud)
如果您知道供应商,这将变得更加简单
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;
}
}
Run Code Online (Sandbox Code Playgroud)
您可以避免通过使用以下方式使字段易变 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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
但是,额外查找是微优化。如果您愿意每个线程执行一次同步命中,则可以完全避免使用volatile。
您的代码不是线程安全的,可以通过剥离所有不相关的部分来轻松显示:
\n\npublic class Memoized<T> {\n private T value;\n // irrelevant parts omitted\n\n public T get() {\n T tempValue = value;\n\n if (tempValue == null) {\n // irrelevant parts omitted\n }\n\n return tempValue;\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n\n因此value没有volatile修饰符,并且您\xe2\x80\x99 在方法中读取它get()而不进行同步,并且当非 时null,继续使用它而不进行任何同步。
无论您在分配时做什么,仅此代码路径就已经使代码被破坏value,因为所有线程安全构造都需要两端(读取端和写入端)使用兼容的同步机制。
事实上,您使用像这样深奥的结构if (_volatile);就变得无关紧要了,因为代码已经被破坏了。
维基百科示例使用带有字段的包装器的原因final是,仅使用final字段的不可变对象不受数据竞争的影响,因此,在没有同步操作的情况下读取其引用时,这是唯一安全的构造。
请注意,由于 lambda 表达式属于同一类别,因此您可以使用它们来简化用例的示例:
\n\npublic class Memoized<T> {\n private boolean initialized;\n private Supplier<T> supplier;\n\n public Memoized(Supplier<T> supplier) {\n this.supplier = () -> {\n synchronized(this) {\n if(!initialized) {\n T value = supplier.get();\n this.supplier = () -> value;\n initialized = true;\n }\n }\n return this.supplier.get();\n };\n }\n\n public T get() {\n return supplier.get();\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n\n这里,supplier.get()withinMemoized.get()可能会在没有同步操作的情况下读取更新的值supplier,在这种情况下它将读取正确的值value,因为它是隐式的final。如果该方法读取了一个过时的参考值supplier,它将最终到达synchronized(this)使用该initialized标志来确定是否需要对原始供应商进行评估的块。
由于该initialized字段只能在synchronized(this)块内访问,因此它将始终评估为正确的值。该块对于每个线程最多执行一次,而只有第一个线程会对get()原始供应商进行评估。之后,每个线程将使用() -> value供应商,返回值而不需要任何同步操作。