Skip to content

设计模式:创建型—单例模式

Published: at 17:27:33

定义

单例模式是属于创建型模式;

其英文定义的原话是:Ensure a class has only one instance,and provide a global point of access to it.

我们通俗点理解就是:一个类只允许创建一个对象(或者实例),那这个类就是一个单例类

如何实现

单例类的实现也非常的简单,实现一个简单的单例类,一般需要注意以下几点:

  1. 构造函数需要是 private 访问权限,这样才能避免外部通过 new 创建实例;
  2. 考虑对象创建时的线程安全问题;
  3. 考虑是否支持延迟加载;
  4. 考虑 getInstance() 性能是否高(是否加锁)。

下面Java语言实现单例的几种常见写法

1.饿汉式

饿汉式的实现方式比较简单。一个“饿”字的精髓就体现于在类加载的时候,instance 静态实例就已经创建并初始化好了。不过,这种方式的缺点也很明显,那就是不支持延迟加载,但是instance 实例的创建过程是线程安全的。

下面是一个简单的单例ID生成器具体的Java实现

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();// 饿汉式的精髓
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() {
    return id.incrementAndGet();
  }
}

采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就可以避免在程序运行的时候,再去初始化导致的性能问题。

2.懒汉式

懒汉式和饿汉式的思想差不多,其相对于饿汉式的优势就是支持延迟加载。

具体的代码实现如下所示:

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance; // 懒汉式,调用getInstance时才实例化
  private IdGenerator() {}
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() {
    return id.incrementAndGet();
  }
}

这种懒汉式的缺点也很明显,给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度会很低,在使用这个单例的时候都将是串行操作。

3.双重检测

懒汉式是给获取实例的方法加了一把大锁,实际上我们只需要在第一次获取的时候需要去初始化类,后面都无需再初始化了,双重检测就是懒汉式的改进版本,即支持延迟加载、又支持高并发(锁粒度减小或无锁),在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了(第一次检测就已经初始化好了)。

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    if (instance == null) { //1. 第一次检测
      synchronized(IdGenerator.class) { // 此处为类级别的锁
        if (instance == null) {  //2. 第二次检测
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  public long getId() {
    return id.incrementAndGet();
  }
}

有人说,这种实现方式有些问题。因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。

要解决这个问题,我们可以给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。

4.静态内部类

再来看一种比双重检测更加简单的实现方法,利用 Java 的静态内部类。

它有点类似饿汉式,但又能做到了延迟加载。

看它的代码实现。

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private IdGenerator() {}

  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }

  public static IdGenerator getInstance() {
    return SingletonHolder.instance;
  }

  public long getId() {
    return id.incrementAndGet();
  }
}

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。

只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。

所以,这种实现方法既保证了线程安全,又能做到延迟加载。

5. 枚举

最后介绍一种最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

具体的代码如下所示:

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);

  public long getId() {
    return id.incrementAndGet();
  }
}

单例存在哪些问题?

  1. 单例对 OOP 特性的支持不友好 OOP 的四大特性是封装、抽象、继承、多态。单例这种设计模式对于其中的抽象、继承、多态都支持得不好。比如上面我们实现的IdGenerator,如果未来某一天,我们希望针对不同的业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方,这样代码的改动就会比较大。
  2. 单例会隐藏类之间的依赖关系 单例无法通过构造函数、参数传递的方式来进行创建,无法通过函数的定义来查看类的依赖关系,具体实现逻辑都是隐藏在内部的,需要仔细查看代码实现才能知道这个类到底依赖哪些类。
  3. 单例对代码的扩展性不友好 如果以后我们要在程序中允许创建两个实例或者多个实例,则需要对单例模式做很大的改动,虽然这种需求很少,但并不是没有,比如线程池。
  4. 单例对代码的可测试性不友好 单例类这种硬编码式的使用方式,会导致在编写单元测试的时候很难实现 对依赖的外部资源进行mock 替换。
  5. 单例不支持有参数的构造函数 单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。 如何解决呢? 1、先调用 init(param) 方法初始化参数,然后调用 getInstance()获取实例。 2、将参数放到 getIntance() 方法中。 3、将参数放到另外一个全局变量中。