设置这个引用是线程安全的吗?

3

我一直得到混合的答案,关于这段代码是否线程安全。我正在使用Java 8。

private final Object lock = new Object();
private volatile Object reference = null;

public Object getOrCompute(Supplier<Object> supplier) {
    if (reference == null) {
        synchronised(lock) {
            if (reference == null) {
                reference = supplier.get();
            }
        }
    }

    return reference;
}

我的期望是,对于这个类的新实例,在多次调用 getOrCompute() 的情况下,只会调用一个供应商,并且该供应商的结果将成为所有调用(以及未来的调用)getOrCompute() 的结果。


2
根据这篇文章,一些非常重要的Java专家认为,由于volatile引用,您的代码是线程安全的。 - Keppil
4
希望 supplier.get() 永远不会返回 null - Andy Brown
@AndyBrown 如果它返回 null,有什么问题吗? - grape_mao
@grape_mao 它将在每次调用 getOrCompute() 时被调用。 - biziclop
1
很好听到你所听到的关于这段代码为什么不是线程安全的论据。 - Chris K
显示剩余3条评论
2个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
3

这是安全的,因为supplier.get()中所做的任何事情都不能与对reference的赋值重新排序。 (更准确地说,在您对reference进行volatile读取时,它不应该被重新排序。)

锁提供了排他性和易变写/读语义提供了可见性。请注意,这只在Java 5之后才成立,那是很久以前发布的,但您仍然会在互联网上找到过时的文章,称双重检查锁定(这是这种惯用语的官方名称)不起作用。当时他们是正确的,但现在已经过时了。

然而,如果供应商提供可变对象,则可能不安全。但那是另一回事。


@Gemtastic 对于像这样的只写字段,两者是等价的。 - biziclop
就我们所知,代码的其他部分可能会将“reference”变量再次转为null,这是不安全的。毕竟,该变量是可访问的。 - Gemtastic
1
@Gemtastic 这个变量是私有的,可以安全地假设 OP 在帖子中包含了所有相关的代码。 - biziclop
1
@Gemtastic 这个问题是关于这段代码是否安全,而不是如果将来添加了一些假设的代码它是否仍然安全。当然,如果您多次设置reference的值,它就不会保持安全,但那将是另一段代码。 - biziclop
@Gemtastic 好的,让我们从不同的角度来看待这个问题。不同线程调用getOrCompute()的顺序是否会影响输出结果?答案是否定的,除非supplier.get()有副作用(但它不应该有)。这意味着该方法是幂等的,并且幂等的代码单元是线程安全的。 - biziclop
显示剩余8条评论

0

同步并不是线程安全的。它阻碍了多个线程同时访问对象,但对于哪个线程首先访问对象,以及获得对象后它会执行什么操作,同步是没有控制的。同步只能限制一次只有一个线程访问对象,第一个访问它的线程会优先访问。

在这种情况下,唯一起作用的是防止多个线程实例化对象。如果对象已经实例化,那么任何想要该对象的线程都可以获得它,而且没有任何线程安全性。

想象一下,你有一个线程正在访问方法和实例化对象,并将其检索出来,而在它检索对象的同时,另一个线程正在尝试实例化它,由于对象已存在,因此它将无法实例化,直接跳转到检索对象,就像第一个线程一样,这些线程现在可以同时修改对象,因此不是线程安全的。但是新对象的实例化在某种程度上是线程安全的,因为该对象只能被实例化一次。


2
在这种情况下,它所做的唯一事情就是防止多个线程实例化对象。它远不止于此,它确保提供的对象对所有后续调用者可见。这就是“安全”的定义。 - biziclop
是的,我将同步块内部的操作称为“实例化”,因为当你将其推向极限时,它就会这样做;它用supplier.get()中的任何内容替换reference的值/引用,因此该部分是线程安全的;只有一个线程可以执行此操作。但是reference对象不是线程安全的,接收它的线程可以随意更改它。 - Gemtastic
2
OP的例子展示了一个众所周知的设计模式——双重检查锁定。双重检查锁定的整个目的就是以线程安全的方式初始化单例对象。因此,如果它唯一的作用就是防止多个线程实例化和使用该类的不同实例,那么它正好做到了它应该做的事情,并且还以线程安全的方式完成了这个任务。 - Solomon Slow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,