有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

无volatile的java双重检查锁定(但使用VarHandle release/acquire)

从某种意义上说,这个问题相当简单。假设我有这个班:

static class Singleton {

}

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

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
}

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

问题是,通过VarHandlejava-9中添加release/acquire语义,我是否可以更快地实现这一点:

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

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


共 (2) 个答案

  1. # 1 楼答案

    我会尝试自己回答这个问题TL;DR:这是一个正确的实现,但可能比使用volatile的实现更昂贵

    虽然这看起来更好,但在某些情况下,它可能表现不佳。我要反对著名的^{独立阅读的独立写作

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

    内容如下:

    • 有两个线程(ThreadAThreadB)分别写入xyx = 1y = 1
    • 还有两个线程(ThreadCThreadD)读取xy,但顺序相反

    因为xyvolatile,下面的结果是不可能的:

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

    这就是{}中的{}所保证的。如果ThreadC观察到对x的写入(它看到x = 1),这意味着ThreadD必须观察相同的x = 1。这是因为在一个连续的执行过程中,一致的写操作好像是以全局顺序发生的,或者好像是原子顺序发生的,在任何地方。所以每个线程都必须看到相同的值。根据to the JLS too的说法,这种执行是不可能的:

    If a program has no data races, then all executions of the program will appear to be sequentially consistent.

    现在,如果我们将相同的示例移动到release/acquirex = 1y = 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本身不允许重新排序,而链式:

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

    允许“反转”(重新排序),因为StoreLoad不是强制性的

    虽然这在某种程度上是错误的和不相关的,因为JLS不能用障碍来解释事情。不幸的是,这些都还没有记录在JLS中


    如果我将其外推到SingletonFactory的例子中,这意味着在发布之后:

     VAR_HANDLE.setRelease(FACTORY, localSingleton);
    

    任何执行^{其他线程:

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

    不保证从发布中读取值(非空Singleton

    想想看:在volatile的情况下,如果一个线程看到了volatile write,那么其他线程肯定也会看到它。对release/acquire没有这种保证

    因此,对于release/acquire,每个线程可能都需要进入同步块。这可能会发生在许多线程中,因为不知道加载release时发生在acquire中的存储何时可见

    即使synchronized本身在下单之前就提供了,这段代码至少在一段时间内(直到观察到发布)的性能会更差吗?(我假设是这样):每个线程都竞争进入同步块

    所以最后,这是关于什么更贵?一个volatile store或一个最终被看到release。我没有答案

  2. # 2 楼答案

    是的,这是正确的,它是存在的。(字段是否易变并不重要,因为它只能从VarHandle访问。)

    如果第一次读取看到一个过时的值,它将进入synchronized块。由于同步块涉及在关系之前发生,所以第二次读取将始终看到写入的值。即使在维基百科上,它也说顺序一致性已经丢失,但它指的是字段;同步块是顺序一致的,即使它们使用了释放-获取语义

    因此,第二次空检查将永远不会成功,并且对象永远不会被实例化两次

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

    在x86上,所有加载都具有acquire语义,因此唯一的开销是空检查。Release acquire允许查看值最终(这就是为什么在Java 9之前,相关方法被称为^{),其Javadoc使用了完全相同的单词)。在这种情况下,同步块会阻止这种情况

    指令不能被重新排序为同步块