无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
}
这就是我们的代码近十年来一直在使用的内容
问题是,通过VarHandle
在java-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;
}
}
这是一个有效且正确的实现吗
# 1 楼答案
我会尝试自己回答这个问题TL;DR:这是一个正确的实现,但可能比使用volatile的实现更昂贵
虽然这看起来更好,但在某些情况下,它可能表现不佳。我要反对著名的^{:独立阅读的独立写作:
内容如下:
ThreadA
和ThreadB
)分别写入x
和y
(x = 1
和y = 1
)ThreadC
和ThreadD
)读取x
和y
,但顺序相反李>因为
x
和y
是volatile
,下面的结果是不可能的:这就是{}中的{}所保证的。如果
ThreadC
观察到对x
的写入(它看到x = 1
),这意味着ThreadD
必须观察相同的x = 1
。这是因为在一个连续的执行过程中,一致的写操作好像是以全局顺序发生的,或者好像是原子顺序发生的,在任何地方。所以每个线程都必须看到相同的值。根据to the JLS too的说法,这种执行是不可能的:现在,如果我们将相同的示例移动到
release/acquire
(x = 1
和y = 1
是释放,而其他读取是获取):结果如下:
这是可能的,也是允许的。这会破坏
sequential consistency
,这是正常的,因为release/acquire
是“较弱的”。因为x86
释放/获取没有施加StoreLoad
障碍,所以acquire
可以在release
之上(重新排序)(不像volatile
禁止这样做)。简单地说volatile
本身不允许重新排序,而链式:允许“反转”(重新排序),因为
StoreLoad
不是强制性的虽然这在某种程度上是错误的和不相关的,因为
JLS
不能用障碍来解释事情。不幸的是,这些都还没有记录在JLS中如果我将其外推到
SingletonFactory
的例子中,这意味着在发布之后:任何执行^{的其他线程:
不保证从发布中读取值(非空
Singleton
)想想看:在
volatile
的情况下,如果一个线程看到了volatile write,那么其他线程肯定也会看到它。对release/acquire
没有这种保证因此,对于
release/acquire
,每个线程可能都需要进入同步块。这可能会发生在许多线程中,因为不知道加载release
时发生在acquire
中的存储何时可见即使
synchronized
本身在下单之前就提供了,这段代码至少在一段时间内(直到观察到发布)的性能会更差吗?(我假设是这样):每个线程都竞争进入同步块所以最后,这是关于什么更贵?一个
volatile store
或一个最终被看到release
。我没有答案# 2 楼答案
是的,这是正确的,它是存在的。(字段是否易变并不重要,因为它只能从
VarHandle
访问。)如果第一次读取看到一个过时的值,它将进入synchronized块。由于同步块涉及在关系之前发生,所以第二次读取将始终看到写入的值。即使在维基百科上,它也说顺序一致性已经丢失,但它指的是字段;同步块是顺序一致的,即使它们使用了释放-获取语义
因此,第二次空检查将永远不会成功,并且对象永远不会被实例化两次
可以保证第二次读取时会看到写入的值,因为执行时持有的锁与计算并存储在变量中的值相同
在x86上,所有加载都具有acquire语义,因此唯一的开销是空检查。Release acquire允许查看值最终(这就是为什么在Java 9之前,相关方法被称为^{),其Javadoc使用了完全相同的单词)。在这种情况下,同步块会阻止这种情况
指令不能被重新排序为同步块