没有volatile的双重检查锁定(但是使用varhandle release/acquire)

pokxtpni  于 2021-07-03  发布在  Java
关注(0)|答案(2)|浏览(370)

在某种程度上,这个问题相当容易。假设我有一节课:

static class Singleton {

}

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

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

我可以摆脱一个 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
}

这几乎就是我们的代码近十年来一直在使用的东西。
问题是我能不能用 release/acquire 添加了语义 java-9 通过 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;
    }

}

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

atmip9wb

atmip9wb1#

是的,这是正确的,维基百科上也有(这个字段是否易变并不重要,因为它只能从 VarHandle .)
如果第一次读取看到一个过时的值,它将进入synchronized块。由于同步块涉及在关系之前发生,因此第二次读取将始终看到写入的值。即使在维基百科上,它也说顺序一致性丢失了,但它指的是字段;同步块是顺序一致的,即使它们使用release-acquire语义。
因此,第二次空检查将永远不会成功,并且对象永远不会示例化两次。
可以保证第二次读取时会看到写入的值,因为执行该值时持有的锁与计算值并存储在变量中时持有的锁相同。
在x86上,所有加载都具有acquire语义,因此唯一的开销是空检查。release-acquire允许最终看到值(这就是调用相关方法的原因) lazySet 在Java9之前,它的javadoc使用了完全相同的词)。在这种情况下,synchronized块阻止了这种情况。
指令不能被重新排序到同步块中。

mm9b1k5b

mm9b1k5b2#

我要亲自回答这个问题。。。热释光;dr:这是一个正确的实现,但可能比使用volatile?的实现更昂贵?。
虽然这看起来更好,但在某些情况下,它可能会表现不佳。我要把自己推向名人的怀抱 IRIW example :独立写入的独立读取:

volatile x, y
     -----------------------------------------------------
     x = 1  |  y = 1   |     int r1 = x   |    int r3 = y
            |          |     int r2 = y   |    int r4 = x

其内容如下:
有两条线( ThreadA 以及 ThreadB )写信给 x 以及 y ( x = 1 以及 y = 1 )
还有两个线程( ThreadC 以及 ThreadD )上面写着 x 以及 y ,但顺序相反。
因为 x 以及 yvolatile 不可能出现以下结果:

r1 = 1 (x)      r3 = 1 (y)
 r2 = 0 (y)      r4 = 0 (x)

这是什么 sequential consistencyvolatile 保证。如果 ThreadC 观察到写入 x (它看到了 x = 1 ),意思是 ThreadD 必须遵守同样的规定 x = 1 . 这是因为在一个连续的一致执行中,写操作好像是以全局顺序发生的,或者好像是原子顺序发生的,无处不在。所以每个线程都必须看到相同的值。因此,根据jls的说法,这种执行是不可能的:
如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的。
现在如果我们把同样的例子移到 release/acquire ( x = 1 以及 y = 1 是释放,而其他读取是获取):

non-volatile x, y
     -----------------------------------------------------
     x = 1  |  y = 1   |     int r1 = x   |    int r3 = y
            |          |     int r2 = y   |    int r4 = x

结果如下:

r1 = 1 (x)      r3 = 1 (y)
r2 = 0 (y)      r4 = 0 (x)

是可能的,也是允许的。这个坏了 sequential consistency 这很正常,因为 release/acquire “较弱”。为了 x86 发布/获取不强制 StoreLoad 障碍,所以 acquire 允许超过(重新排序)一个 release (不像 volatile 这是禁止的)。简单地说 volatile s本身不允许重新订购,而类似于:

release ... // (STORE)
 acquire ... // this acquire (LOAD) can float ABOVE the release

允许“反转”(重新排序),因为 StoreLoad 不是强制性的。
虽然这是错误的和无关的,因为 JLS 不能用障碍来解释事情。不幸的是,这些还没有记录在jls中。。。
如果我把这推到 SingletonFactory ,这意味着发布后:

VAR_HANDLE.setRelease(FACTORY, localSingleton);

任何其他执行 acquire :

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

不保证从版本中读取值(非空 Singleton ).
想想看:万一 volatile ,如果一个线程看到了volatile write,那么其他线程肯定也会看到它。没有这样的保证 release/acquire .
因此,与 release/acquire 每个线程可能都需要进入同步块。这种情况可能会发生在许多线程中,因为在 release 将通过负载可见 acquire .
即使 synchronized 它本身并没有发生在订单之前,这段代码,至少在一段时间内(直到发布被观察到)会表现得更糟吗(我假设是这样的:每个线程都竞相进入同步块。
所以最后-这是关于什么更贵?一 volatile store 或者一个最终被看到的 release . 我没有答案。

相关问题