没有 volatile 的双重检查锁定(但带有 VarHandle 释放/获取)

Eug*_*ene 7 java concurrency volatile double-checked-locking

在某种程度上,这个问题相当简单。假设我有这个类:

static class Singleton {

}
Run Code Online (Sandbox Code Playgroud)

我想为它提供一个单例工厂。我可以做(可能)显而易见的事情。我不打算提及枚举的可能性或任何其他可能性,因为我对它们不感兴趣。

static final class SingletonFactory {

    private static volatile Singleton singleton;

    public static Singleton getSingleton() {
        if (singleton == null) { // volatile read
            synchronized (SingletonFactory.class) {
                if (singleton == null) { // volatile read
                    singleton = new Singleton(); // volatile write
                }
            }
        }
        return singleton; // volatile read
    }
}
Run Code Online (Sandbox Code Playgroud)

我可以volatile read以更高的代码复杂性为代价摆脱一个:

public static Singleton improvedGetSingleton() {
    Singleton local = singleton; // volatile read
    if (local == null) {
        synchronized (SingletonFactory.class) {
           local = singleton; // volatile read
           if (local == null) {
               local = new Singleton();
               singleton = local; // volatile write
           }
        }
    }

    return local; // NON volatile read
}
Run Code Online (Sandbox Code Playgroud)

这几乎是我们的代码近十年来一直在使用的。

问题是我可以通过release/acquire添加语义来更快地做到这一点:java-9VarHandle

static final class SingletonFactory {

    private static final SingletonFactory FACTORY = new SingletonFactory();

    private Singleton singleton;

    private static final VarHandle VAR_HANDLE;

    static {
        try {
            VAR_HANDLE = MethodHandles.lookup().findVarHandle(SingletonFactory.class, "singleton", Singleton.class);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Singleton getInnerSingleton() {

        Singleton localSingleton = (Singleton) VAR_HANDLE.getAcquire(FACTORY); // acquire

        if (localSingleton == null) {
            synchronized (SingletonFactory.class) {
                localSingleton = (Singleton) VAR_HANDLE.getAcquire(FACTORY); // acquire
                if (localSingleton == null) {
                    localSingleton = new Singleton();
                    VAR_HANDLE.setRelease(FACTORY, localSingleton); // release
                }
            }
        }

        return localSingleton;
    }
    
}
Run Code Online (Sandbox Code Playgroud)

这会是一个有效且正确的实现吗?

Fra*_*ani 7

是的,这是正确的,它出现在 Wikipedia 上。(该字段是 volatile 并不重要,因为它只能从VarHandle.)

如果第一次读取看到一个陈旧的值,它就会进入同步块。由于同步块涉及先发生关系,第二次读取将始终看到写入的值。即使在维基百科上,它也说失去了顺序一致性,但它指的是字段;同步块是顺序一致的,即使它们使用释放-获取语义。

所以第二次空检查永远不会成功,对象永远不会被实例化两次。

保证第二次读取将看到写入的值,因为它是在与计算值并存储在变量中时持有的锁相同的情况下执行的。

在 x86 上,所有负载都具有获取语义,因此唯一的开销是空检查。Release-acquire 允许最终看到值(这就是为什么lazySet在 Java 9 之前调用相关方法,并且它的 Javadoc 使用完全相同的词)。在这种情况下,同步块可以防止这种情况发生。

指令不能被重新排序并进入同步块。