23种经典设计模式-单例模式-创建型


简介

单例设计模式(Singleton Design Pattern):
创建型设计模式,一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,并提供一个访问该实例的全局节点,这种设计模式就叫作单例设计模式,简称单例模式。

要点

为什么我们需要单例这种设计模式?它能解决哪些问题?

  1. 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
  2. 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  3. 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  4. 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  5. 频繁访问数据库或文件的对象。
  6. 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  7. 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

问题

单例存在哪些问题?

  1. 单例对 OOP 特性的支持不友好
    单例这种设计模式对于其中的继承、多态都支持得不友好。
    从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,会导致代码的可读性变差。
    不明白设计意图的人,看到这样的设计,会觉得莫名其妙。所以,一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。
  2. 单例会隐藏类之间的依赖关系
    通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。
    但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。
    如果代码比较复杂,这种调用关系就会非常隐蔽。
    在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。
  3. 单例对代码的扩展性不友好
    单例类只能有一个对象实例。
    如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。
  4. 单例对代码的可测试性不友好
    如果单例类持有成员变量(比如 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。
    如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题。
  5. 单例不支持有参数的构造函数
    单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。

单例与静态的区别?

单例

  1. 单例可以继承类,实现接口,而静态类不能(可以集成类,但不能集成实例成员)。
  2. 单例可以被延迟初始化,静态类一般在第一次加载是初始化。
  3. 单例类可以被集成,他的方法可以被覆写。
  4. 单例类可以被用于多态而无需强迫用户只假定唯一的实例。举个例子,你可能在开始时只写一个配置,但是以后你可能需要支持超过一个配 置集,或者可能需要允许用户从外部从外部文件中加载一个配置对象,或者编写自己的。你的代码不需要关注全局的状态,因此你的代码会更加灵活。

静态

静态方法中产生的对象,会随着静态方法执行完毕而释放掉,而且执行类中的静态方法时,不会实例化静态方法所在的类。
如果是用singleton,产生的那一个唯一的实例,会一直在内存中,不会被GC清除的(原因是静态的属性变量不会被GC清除),除非整个JVM退出了。

代码示例

饿汉式

在类加载的时候,instance静态实例就已经创建并初始化好了,所以instance实例的创建过程是线程安全的。

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();
  }
}

懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载。
不过懒汉式的缺点也很明显,我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。
量化一下的话,并发度是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。
但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈。

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

双重检测

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。
双重检测实现方式既支持延迟加载、又支持高并发的单例实现方式。
只要 instance 被创建之后,再调用 getInstance() 函数都不会进入到加锁逻辑中。
所以,这种实现方式解决了懒汉式并发度低的问题。

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

这种实现方式有些问题。
因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。
要解决这个问题,我们需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。
实际上,只有很低版本的 Java 才会有这个问题。
我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。

静态内部类

利用Java的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。
SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。
只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。
所以,这种实现方法既保证了线程安全,又能做到延迟加载。

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();
  }
}

枚举

Java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

参考资料

王争-设计模式之美
https://time.geekbang.org/column/intro/100039001
Refactoring.Guru
https://refactoringguru.cn/design-patterns/singleton


文章作者: Baymax
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Baymax !
评论
  目录