没有 volatile 的双重检查锁定(但带有 VarHandle 释放/获取) [英] double check locking without volatile (but with VarHandle release/acquire)

查看:15
本文介绍了没有 volatile 的双重检查锁定(但带有 VarHandle 释放/获取)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这个问题在某种程度上很简单.假设我有这个类:

The question is rather easy, in a way. Suppose I have this class:

static class Singleton {

}

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

And I want to provide a singleton factory for it. I can do the (probably) obvious. I am not going to mention the enum possibility or any other, as they are of no interest to me.

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
    }
}

我可以以更高的代码复杂性为代价摆脱一次易失性读取:

I can get away from one volatile read with the price of higher code complexity:

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
}

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

This is pretty much what our code has been using for close to a decade now.

问题是我可以通过 VarHandlejava-9 中添加 release/acquire 语义来使这更快:

The question is can I make this even faster with release/acquire semantics added in java-9 via VarHandle:

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;
    }
    
}

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

Would this be a valid and correct implementation?

推荐答案

是的,这是正确的,并且存在 维基百科.(该字段是可变的并不重要,因为它只能从 VarHandle 访问.)

Yes, this is correct, and it is present on Wikipedia. (It doesn't matter that the field is volatile, since it is only ever accessed from VarHandle.)

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

If the first read sees a stale value, it enters the synchronized block. Since synchronized blocks involve happen-before relationships, the second read will always see the written value. Even on Wikipedia it says sequential consistency is lost, but it refers to the fields; synchronized blocks are sequentially consistent, even though they use release-acquire semantics.

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

So the second null check will never succeed, and the object is never instantiated twice.

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

It is guaranteed that the second read will see the written value, because it is executed with the same lock held as when the value was computed and stored in the variable.

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

On x86 all loads have acquire semantics, so the only overhead would be the null check. Release-acquire allows values to be seen eventually (that's why the relevant method was called lazySet before Java 9, and its Javadoc used that exact same word). This is prevented in this scenario by the synchronized block.

指令可能不会被重新排序并放入同步块中.

Instructions may not be reordered out and into synchronized blocks.

这篇关于没有 volatile 的双重检查锁定(但带有 VarHandle 释放/获取)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆