在线程安全单例中,返回是否必须在同步块内

dre*_*ash 7 java multithreading synchronization thread-safety

考虑以下代码:

private static Singleton singleton;

public static Singleton get(){
    synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
    } 
    return singleton; // <-- this part is important
}
Run Code Online (Sandbox Code Playgroud)

这是这个问题的后续讨论。最初,我认为它是线程安全的。然而,一些受人尊敬的用户认为,由于块的return singleton外部,这不是线程安全的synchronized。然而,其他一些(受人尊敬的)用户则反对。

在我阅读了do we need volatile when implementations singleton using double-check locked 之后,我改变了主意。(来自那个问题的代码):

    private static Singleton instance;

    private static Object lock = new Object();

    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (lock) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
Run Code Online (Sandbox Code Playgroud)

(众所周知,为什么volatile第二个代码需要 。)

但是,在再次查看这两个示例后,我注意到第一个和第二个代码片段之间存在很大差异。在前者中,最外层if是在synchronized子句内,因此在synchronized块内运行的所有线程都将强制发生先发生关系(即,null如果实例设置正确,线程将无法返回)或者我错了?我希望采取以下行动顺序:

lock monitor
...
unlock monitor
...
read singleton
Run Code Online (Sandbox Code Playgroud)

我注意到所有与第一个代码片段相似的在线示例在synchronized块内都有返回;然而,这可能仅仅是因为在性能方面它是相同的,因为线程必须同步离开,所以为什么不安全起见并将返回值放在里面呢?!.

题:

返回真的需要在synchronized块内吗?return 语句的单例值的读取能否在synchronized块开始之前看到单例值?

Gra*_*ray 2

返回值真的需要位于同步块内吗?

否,return不需要位于synchronized块中,除非singleton字段可以分配到其他地方。但是,没有充分的理由说明为什么return不应位于同步块内。如果整个方法都包装在同步中,那么如果我们在此处的类中,则可以将该方法标记为同步Singleton。如果单例在其他地方被修改,这会更干净、更好。

就为什么它不需要在内部而言,因为您使用的是synchronized块,所以在块的开头有一个读屏障,在末尾有一个写屏障,这意味着线程将获得最多的资源的最新值singleton并且只会被分配一次。

读取内存屏障确保线程将看到更新的单例,该单例将是null或完全发布的对象。写内存屏障确保任何更新都singleton将写入主内存,其中包括完整构建Singleton并将其发布到字段singleton。程序顺序保证块singleton内分配的synchronized值将作为相同的值返回,除非另一个线程中存在另一个分配,否则singleton它将是未定义的。

如果您执行以下操作,程序顺序将更加有效。我倾向于在(使用适当的双重检查锁定代码)时执行此singleton操作volatile

synchronized (Singleton.class) {
    Singleton value = singleton;
    if (singleton == null) {
       value = new Singleton();
       singleton = value;
    }
    return value;
}
Run Code Online (Sandbox Code Playgroud)

不是线程安全的,因为返回单例位于同步块之外

由于您使用的是synchronized块,所以这不是问题。正如您所指出的,双重检查锁定就是为了避免synchronized每次操作时都碰到块。

在同步块中运行的所有线程将强制发生先于关系(即,如果正确设置了实例,线程将无法返回 null)还是我错了?

这是正确的。你没有错。

然而,这可能只是因为性能方面它是相同的,因为线程必须同步,所以为什么不为了安全起见并将返回值放在里面呢?!

没有理由不这样做,尽管我认为“安全方面”更多的是在其他人审查此代码并在将来担心它时引起恐慌,而不是从语言定义的角度来看“更安全”。同样,如果还有其他地方singleton被分配,那么return应该在块的内部synchronized

  • @Eugene,“同步”块和使用同一监视器的“同步”块的任何先前执行之间存在“发生在”关系。然后,由于程序顺序,在“synchronized”块和该线程内的后续语句之间有一个“happens-before”。因此,由于传递性,在该“同步”块的先前执行与后续语句之间存在“发生之前”。如果其他执行也可以写入该变量,那就太好了,但“同步”块中的逻辑确保最多有一次写入。 (3认同)
  • @Eugene,虽然我不喜欢使用术语“障碍”,但只要您记住哪些运动是可能的以及哪些障碍负责正确性,锁定粗化并不矛盾。`return` 可以移到写屏障之前,即放在 `synchronized` 块内(但我们都知道将它放在 `synchronized` 块内是正确的),但仍然位于写屏障开头的读屏障之后。 “同步”块,因此它仍然可以看到离开“同步”块/通过写入屏障的最后一个线程的最新写入。 (2认同)