在某种程度上,这个问题相当容易。假设我有一节课:
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;
}
}
这是一个有效和正确的实现吗?
2条答案
按热度按时间atmip9wb1#
是的,这是正确的,维基百科上也有(这个字段是否易变并不重要,因为它只能从
VarHandle
.)如果第一次读取看到一个过时的值,它将进入synchronized块。由于同步块涉及在关系之前发生,因此第二次读取将始终看到写入的值。即使在维基百科上,它也说顺序一致性丢失了,但它指的是字段;同步块是顺序一致的,即使它们使用release-acquire语义。
因此,第二次空检查将永远不会成功,并且对象永远不会示例化两次。
可以保证第二次读取时会看到写入的值,因为执行该值时持有的锁与计算值并存储在变量中时持有的锁相同。
在x86上,所有加载都具有acquire语义,因此唯一的开销是空检查。release-acquire允许最终看到值(这就是调用相关方法的原因)
lazySet
在Java9之前,它的javadoc使用了完全相同的词)。在这种情况下,synchronized块阻止了这种情况。指令不能被重新排序到同步块中。
mm9b1k5b2#
我要亲自回答这个问题。。。热释光;dr:这是一个正确的实现,但可能比使用volatile?的实现更昂贵?。
虽然这看起来更好,但在某些情况下,它可能会表现不佳。我要把自己推向名人的怀抱
IRIW example
:独立写入的独立读取:其内容如下:
有两条线(
ThreadA
以及ThreadB
)写信给x
以及y
(x = 1
以及y = 1
)还有两个线程(
ThreadC
以及ThreadD
)上面写着x
以及y
,但顺序相反。因为
x
以及y
是volatile
不可能出现以下结果:这是什么
sequential consistency
的volatile
保证。如果ThreadC
观察到写入x
(它看到了x = 1
),意思是ThreadD
必须遵守同样的规定x = 1
. 这是因为在一个连续的一致执行中,写操作好像是以全局顺序发生的,或者好像是原子顺序发生的,无处不在。所以每个线程都必须看到相同的值。因此,根据jls的说法,这种执行是不可能的:如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的。
现在如果我们把同样的例子移到
release/acquire
(x = 1
以及y = 1
是释放,而其他读取是获取):结果如下:
是可能的,也是允许的。这个坏了
sequential consistency
这很正常,因为release/acquire
“较弱”。为了x86
发布/获取不强制StoreLoad
障碍,所以acquire
允许超过(重新排序)一个release
(不像volatile
这是禁止的)。简单地说volatile
s本身不允许重新订购,而类似于:允许“反转”(重新排序),因为
StoreLoad
不是强制性的。虽然这是错误的和无关的,因为
JLS
不能用障碍来解释事情。不幸的是,这些还没有记录在jls中。。。如果我把这推到
SingletonFactory
,这意味着发布后:任何其他执行
acquire
:不保证从版本中读取值(非空
Singleton
).想想看:万一
volatile
,如果一个线程看到了volatile write,那么其他线程肯定也会看到它。没有这样的保证release/acquire
.因此,与
release/acquire
每个线程可能都需要进入同步块。这种情况可能会发生在许多线程中,因为在release
将通过负载可见acquire
.即使
synchronized
它本身并没有发生在订单之前,这段代码,至少在一段时间内(直到发布被观察到)会表现得更糟吗(我假设是这样的:每个线程都竞相进入同步块。所以最后-这是关于什么更贵?一
volatile store
或者一个最终被看到的release
. 我没有答案。