懒惰的初始化/记忆,无波动

时间:2019-01-08 21:20:03

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

似乎Java内存模型没有定义本地缓存的“刷新”和“刷新”,相反,人们只是为了简单起见才这样称呼它,但实际上“ happens-before”关系意味着以某种方式刷新和刷新(如果您能解释一下就很好,但不能直接成为问题的一部分。

这使我感到非常困惑,因为关于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,而无需再次查看主内存。

2 个答案:

答案 0 :(得分:4)

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

答案 1 :(得分:0)

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

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时在做什么,因为所有线程安全构造都需要两端(读写侧)才能使用兼容的同步机制。 / p>

由于代码已被破坏,因此您使用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();
    }
}

在这里,supplier.get()中的Memoized.get()可以读取supplier的更新值而无需同步操作,在这种情况下,它将读取正确的value,因为它是隐式的{ {1}}。如果该方法为final引用读取了过时的值,则它将终止于supplier块,该块使用synchronized(this)标志来确定是否需要对原始供应商进行评估。 / p>

由于仅在initialized块内访问initialized字段,因此它将始终计算为正确的值。每个线程最多只能执行一次该块,而只有第一个线程会在原始供应商上评估synchronized(this)。之后,每个线程将使用get()供应商,无需任何同步操作即可返回值。