面试官:如何设计Singleton单例?

这是自从 Java 流行以来,一个非常流行的 Java 面试问题!这是一个非常简单的问题,为面试官打开了整个房间,可以提出很多后续问题。可以评估 Java 基础知识、设计模式知识,甚至扩展到多线程/其他 LLD 场景。

解决这个难题有三种方法:基本解决方案、线程安全解决方案和优化的线程安全解决方案。

最好的方法是用一个简单的类开始解释该场景上下文:

class Singleton{
  private Singleton(){}
  private static Singleton obj;

  public static Singleton getInstance(){
    if(obj==null){
      obj = new Singleton();
    }
    return obj;
  }
}

这些是最低限度的理解:

  • 私有构造函数
  • 保存单例对象值的静态对象(类级别)
  • 返回对象的静态方法

考生在这里通常会犯的一个错误是把静态变量的使用解释为线程安全的一种变通办法。

请注意,静态变量是默认情况下所有线程共享的类级变量。
实例变量不需要同步,除非它暴露在多线程场景中。

因此,静态变量无论如何都不是线程安全的。在这里,静态变量只是用来表示一个单例对象,这意味着在整个应用程序生命周期中只有一个对象可用。

下一个问题是"这段代码线程安全吗?"
当然不!

因此,我们首先想到的是同步该方法,从而引入一个内在锁。这样一来,共享的可变状态就变成了原子可访问状态,而这正是我们想要的线程安全:

class Singleton{
  private Singleton(){}
  private static Singleton obj;

  public static synchronized Singleton getInstance(){
    if(obj==null){
      obj = new Singleton();
    }
    return obj;
  }
}

但是,这里又会有什么问题呢?
我们正在对整个方法执行同步锁:获取和释放锁是一个昂贵的操作。我们要确保同步足够覆盖所有可变状态。
我们不应该同步太多或太少。
因此,这里存在一个性能问题。

此外,变量 obj 可能会被缓存到各个线程中。因此,obj 状态仍有可能出现竞赛条件。我们也需要对此进行反思。

下面是解决了线程安全问题的优化版本:

class Singleton{
  private Singleton(){}
  private static volatile Singleton obj;

  public static synchronized Singleton getInstance(){
    if(obj==null){
      synchronized(this){
        if(obj==null){
            obj = new Singleton();
        }
      }
    }
    return obj;
  }
}

请注意,使用 volatile 变量是为了解决可见性问题。与普通变量不同,volatile 变量可确保将更改推送到共享内存,而不是持久存在线程的本地缓存中。这些变量的值将从 RAM(共享内存)中读取。因此,如果一个线程更改了 obj 的值,那么其他试图使用 obj 的线程也会看到这些更改。

由于我们没有同步整个方法,因此多个线程可以并发访问该方法,并检查 obj 的值是否为空。线程可以在不获取锁的情况下执行此检查。现在有以下几种可能的情况:

  • 线程 A 发现 obj 的值已被设置。只需返回该值即可。
  • 线程 A 进入并获取了 obj 的锁。如果尚未更改,则更改 obj 的值。
  • 线程 A 进入并观察到线程 B 当前持有锁。线程 B 完成执行并更改了 obj 的值。线程 A 恢复操作,并获得锁。由于 obj 的值已经更改,因此只需返回该值即可。

需要注意的是,volatile 关键字并不能保证线程安全。所以,反问句应该是"那为什么我们之前提到它是最佳解决方案呢?其实,这是有问题的:当有多个读取线程,但只有一个写入线程时,volatile 关键字会带来线程安全!

因此,只要事实如此,优化后的解决方案仍然是线程安全的。

因此,作为线程安全的最后措施,我们可以使用 Java 内置的线程安全类:如 AtomicReference。
AtomicReference 是一个线程安全类,能让我们对多个可变状态执行原子复合操作。我们可以将所有可变状态封装到一个 AtomicReference 对象中,然后用它来原子访问/修改值。

因此,解决方案可以稍微修改如下:

public class Singleton{
  private Singleton(){}
  private static AtomicReference<Singleton> obj;

  public static synchronized SingleTon getInstance(){
    if(obj==null){
      synchronized(SingleTon.class){
        if(obj==null){
          // set the value of Singleton object and set here
          obj = new AtomicReference<>();
        }
      }
    }
    return obj.get();
  }

}