首页 > 解决方案 > 创建单例问题的双重检查方式

问题描述

读取(关键资源块外)和写入(关键资源块内)如何不存在原子性问题。

我已经阅读并与不同的人讨论过,但大多数人没有回答这两个操作是否都是原子的,以及上述问题实际上是如何实现原子性的。

class ABC {
    private static volatile ABC abcInstance;
    static ABC getInstance(){
        if(abcInstance == null){
            synchronized(ABC.class){
                if(abcInstance == null){
                    abcInstance = new ABC();
                    return abcInstance;
                }
            }
        }
        return abcInstance;
    }

}

if(abcInstance == null) outside synchronisation blockabcInstance = new ABC();原子的,如果不是,那么这种创建单例的方式是错误的。

在 C++ 中,abcInstance = new ABC();广义上由三个指令组成:

  1. 创建 ABC 对象。
  2. 为 ABC 分配内存。
  3. 将其分配给 abcInstance。

并且为了优化编译器可以以任何方式重新排序这三个指令。假设它遵循 2->3->1 并且在指令 3 中断发生之后,下一个调用 getInstance() 的线程将读取 abcInstance 有一些值,然后它将指向没有 ABC 对象的东西。

如果 C++ 和 Java 都错了,请纠正我。

标签: javac++singleton

解决方案


这仅回答了您问题的 Java 部分。

if(abcInstance == null)并且abcInstance = new ABC();是原子的,如果不是,那么这种创建单例的方式是错误的。

(潜在的)问题不是原子性。(从执行分配的线程和读取分配变量的线程的角度来看,引用分配是原子的。)

问题是当写入的值abcInstance对另一个线程可见时。

  • 在 Java 5 之前,内存模型并没有为该实现可靠地工作提供足够的内存可见性保证。

  • 在 Java 5(及更高版本)内存模型中,一个线程对volatile变量的写入与另一个线程随后对该变量的读取之间存在着先发生关系。这表示:

    1. abcInstance如果第一个线程已写入,则保证第二个线程可以看到非空值。
    2. 发生之前的关系还保证第二个线程将看到ABC由第一个线程创建的实例的完全初始化状态。
    3. synchronized块确保一次只能ABC创建一个实例。

这是解释为什么旧的双重检查锁定实现被破坏的权威文章:


正如 Andrew Turner 所说,在 Java 中实现单例类有一种更简单、更简洁的方法:使用enum.


推荐阅读