【问题标题】:Lazy initialization / memoization without volatile没有 volatile 的延迟初始化/记忆
【发布时间】:2019-06-03 14:56:41
【问题描述】:

看起来 Java 内存模型没有定义本地缓存的“刷新”和“刷新”,相反人们只是为了简单起见才这样称呼它,但实际上“发生前”关系意味着刷新和刷新以某种方式(会如果您能解释这一点,那就太好了,但不是问题的直接部分)。

这让我很困惑,再加上关于Java Memory Model in the JLS 的部分并不是以易于理解的方式编写的。

因此,请您告诉我我在以下代码中所做的假设是否正确,因此是否可以保证正确运行?

它部分基于Double-checked locking 上的 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;
    }
}

我还读到构造函数调用可以被内联和重新排序,从而导致对未初始化对象的引用(参见this comment on a blog)。那么直接分配供应商的结果是否安全,还是必须分两步完成?

value = tempValue = supplier.get();

两步:

tempValue = supplier.get();
// Reorder barrier, maybe not needed?
if (_volatile);
value = tempValue;

编辑:这个问题的标题有点误导,目的是减少 volatile 字段的使用。如果初始化值已经在某个线程的缓存中,则直接访问value,无需再去主存中查找。

【问题讨论】:

  • 你的 volatile 没有用,因为你没有给它赋值。 “刷新”发生在 synchronized 块的末尾,因为根据 JMM,锁的释放发生在随后获取同一锁之前
  • final 的双重检查锁定示例简要说明了here on Stack Overflow(也由 Aleksey Shipilev 撰写)。
  • "你的 volatile 没有用,因为你没有给它赋值。" @Ivan,我用它来创建发生前的关系,而不是在其中存储任何价值。
  • 发生在我确定写入 volatile 和后续读取之间。您不会向该变量写入任何内容。

标签: java memoization lazy-initialization java-memory-model


【解决方案1】:

如果你只有几个单例,你可以减少 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。

【讨论】:

  • 感谢您的回答,但我不想创建单例,我想懒惰地初始化任意值。
  • 我也不想使用Unsafe,但是感谢提供这个解决方案,我不知道这个选项
【解决方案2】:

您的代码不是线程安全的,可以通过剥离所有不相关的部分来轻松显示:

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); 这样的深奥结构这一事实就变得无关紧要了,因为代码已经被破坏了。

Wikipedia 示例使用带有 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()。之后,每个线程将使用() -&gt; value供应商,返回值而不需要任何同步操作。

【讨论】:

  • 如果读取过时的supplier 并输入和退出synchronized(this),由于同步块的发生前保证,将读取新的supplier,对吗?您是否有安全发布 lambdas 的来源(如果可能,JLS)?
  • 您知道这种方法与其他解决方案相比表现如何吗? Aleksey Shipilёv 制作了 performance tests,但这不包括 lambdas。
  • 此外,我将把这个答案标记为已接受,因为它指出了我的解决方案有什么问题,尽管 Peter Lawrey 提供了很好的替代解决方案。
  • 是的,synchronized(this) 提供了必要的happens-before 关系。 lambda 表达式的捕获值的安全性没有明确提及,但可以从局部变量为final 或有效最终且在 lambda 表达式之前明确分配的要求(根据 JLS§15.27.2.)推导出来。在实践中,这些值被复制到生成类的final 字段中(并且任何不需要类的假设未来机制都必须与之保持兼容)。性能预计介于 Final Wrapper 和 Holder 方法之间。
猜你喜欢
  • 1970-01-01
  • 2020-05-10
  • 1970-01-01
  • 2011-11-17
  • 1970-01-01
  • 2020-10-08
  • 2017-11-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多