设计模式(1/23) - 单例模式

101 阅读8分钟

单例模式

1 概述

  • 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
  • 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
  • 单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
  • 注意:
  • 1)单例类只能有一个实例。
  • 2)单例类必须自己创建自己的唯一实例。
  • 3)单例类必须给所有其他对象提供这一实例。

2 优缺点及应用场景

2.1 优点

  • 1)唯一实例:保证系统中只有一个实例,避免不一致的状态。
  • 2)全局访问:提供一个全局访问点,方便获取实例。
  • 3)序列号:生成唯一序列号。

2.2 缺点

  • 1)不易扩展:单例模式对扩展不友好,难以进行子类化。
  • 2)隐藏依赖:使代码与全局实例耦合,增加了代码的复杂性和难以测试性。

2.3 应用场景

  • 1)日志记录器:日志记录器通常作为单例存在,以确保日志信息的一致性和顺序性,同时避免多实例导致的资源竞争。
  • 2)配置管理器:在应用程序中,配置管理器通常只需要一个实例,用于存储和访问应用程序的配置信息。

3 结构

  • 1)单例类:包含单例实例的类,通常将构造函数声明为私有。
  • 2)静态成员变量:用于存储单例实例的静态成员变量。
  • 3)获取实例方法:静态方法,用于获取单例实例。
  • 4)私有构造函数:防止外部直接实例化单例类。
  • 5)线程安全处理:确保在多线程环境下单例实例的创建是安全的。

4 实现

4.1 UML 类图

单例模式.jpg

4.2 代码示例

4.2.1 单例模式(java 双重判空加锁)
/**
 * 题目:
 * 单例模式,双重判空加锁。
 * <p>
 * 解题思路:
 * synchronized 的作用?
 * 假设有两个线程同时想创建一个实例,由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁时,
 * 第二个线程只能等待。当第一个线程发现实例还没有创建,它创建出一个实例。接着第一个线程释放同步锁,
 * 此时第二个线程可以加上同步锁,并运行接下来的代码。这时候由于实例已经被第一个线程创建出来了,
 * 第二个线程就不会重复创建实例了,这样就保证了我们在多线程环境中也只能得到一个实例。
 * 但是这里有个缺陷,每次通过 Instance 得到其实例的时候,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免。
 * <p>
 * volatile 关键字的作用?
 * 双重检锁单例模式在 CPU 的工作流,主要分为三步;
 * 1:分配内存对象空间;
 * 2:初始化对象;
 * 3:设置 instance 指向刚才分配的内存地址,注意 jvm 和 cpu 优化会指令重排,上面顺序会变成 1-3-2,
 * 单线程环境下,此顺序是没有问题,2 和 3 前后没有依赖性,但是在多线程情况下会有这种情况,
 * 当线程 A 在执行第 5 行代码时,B线程进来执行到第 2 行代码。假设此时 A 执行的过程中发生了指令重排序,
 * 即先执行了 1 和 3,没有执行 2。那么由于A线程执行了 3 导致 instance 指向了一段地址,
 * 所以 B 线程判断 instance 不为 null,会直接跳到第 6 行并返回一个未初始化的对象。
 * 结论:volatile 保持指令的有序性,能够有效禁止指令重排序。
 */
public class SingletonPattern1 {

  private static volatile SingletonPattern1 instance = null;

  private SingletonPattern1() {

  }

  public static SingletonPattern1 getInstance() {
    if (null == instance) {
      synchronized (SingletonPattern1.class) {
        if (null == instance) {
          instance = new SingletonPattern1();
        }
      }
    }
    return instance;
  }

}
4.2.2 单例模式(java 静态代码块)
/**
 * 题目:
 * 单例模式,静态代码块。
 * <p>
 * 解题思路:
 * 静态代码块只在类加载的时候调用一次(静态方法调用等第一次用到该类的时候),后续不再调用;
 * 使用静态代码块,如果该类中还有其他静态方法,调用后就会执行静态代码块使得对象过早创建;
 * 使用一个静态类来创建 Singleton,其他静态方法只要没有调用 Nested.singletonImp6 就不会创建 Singleton;
 * 实现了需要时才创建实例对象,避免过早创建。
 */
public class SingletonPattern2 {

  private SingletonPattern2() {

  }

  private static class Holder {
    public static SingletonPattern2 instance = new SingletonPattern2();
  }

  public static SingletonPattern2 getInstance() {
    return Holder.instance;
  }

}
4.2.3 单例模式(kotlin 双重判空加锁)
/**
 * 题目:
 * 单例模式,双重判空加锁。
 * <p>
 * 解题思路:
 * synchronized 的作用?
 * 假设有两个线程同时想创建一个实例,由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁时,
 * 第二个线程只能等待。当第一个线程发现实例还没有创建,它创建出一个实例。接着第一个线程释放同步锁,
 * 此时第二个线程可以加上同步锁,并运行接下来的代码。这时候由于实例已经被第一个线程创建出来了,
 * 第二个线程就不会重复创建实例了,这样就保证了我们在多线程环境中也只能得到一个实例。
 * 但是这里有个缺陷,每次通过 Instance 得到其实例的时候,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免。
 * <p>
 * volatile 关键字的作用?
 * 双重检锁单例模式在 CPU 的工作流,主要分为三步;
 * 1:分配内存对象空间;
 * 2:初始化对象;
 * 3:设置 instance 指向刚才分配的内存地址,注意 jvm 和 cpu 优化会指令重排,上面顺序会变成 1-3-2,
 * 单线程环境下,此顺序是没有问题,2 和 3 前后没有依赖性,但是在多线程情况下会有这种情况,
 * 当线程 A 在执行第 5 行代码时,B线程进来执行到第 2 行代码。假设此时 A 执行的过程中发生了指令重排序,
 * 即先执行了 1 和 3,没有执行 2。那么由于A线程执行了 3 导致 instance 指向了一段地址,
 * 所以 B 线程判断 instance 不为 null,会直接跳到第 6 行并返回一个未初始化的对象。
 * 结论:volatile 保持指令的有序性,能够有效禁止指令重排序。
 */
class SingletonPattern3 private constructor() {

  companion object {
    @Volatile
    private var instance: SingletonPattern3? = null

    @JvmStatic
    fun getInstance(): SingletonPattern3 {
      return instance ?: synchronized(this) {
        instance ?: SingletonPattern3().also {instance = it}
      }
    }

  }

}
4.2.4 单例模式(kotlin 静态代码块)
/**
 * 题目:
 * 单例模式,静态代码块。
 * <p>
 * 解题思路:
 * 静态代码块只在类加载的时候调用一次(静态方法调用等第一次用到该类的时候),后续不再调用;
 * 使用静态代码块,如果该类中还有其他静态方法,调用后就会执行静态代码块使得对象过早创建;
 * 使用一个静态类来创建 Singleton,其他静态方法只要没有调用 Nested.singletonImp6 就不会创建 Singleton;
 * 实现了需要时才创建实例对象,避免过早创建。
 */
class SingletonPattern4 private constructor() {

  companion object {
    val instance = Holder.instance

  }

  private object Holder {
    val instance = SingletonPattern4()
  }

}
4.2.4 单例模式(kotlin by lazy)
/**
 * 题目:
 * 单例模式,by lazy。
 * <p>
 * 解题思路:
 * lateinit 和 by lazy 的区别?
 * lateinit 只能用于修饰变量 var,不能用于可空的属性和 Java 的基本类型;
 * lateinit 可以在任何位置初始化并且可以初始化多次;
 * lazy() 只能用于修饰常量 val,并且 lazy() 是线程安全的;
 * lazy() 是一个函数,可以接受一个 Lambda 表达式作为参数,第一次调用时会执行 Lambda 表达式,以后调用该属性会返回之前的结果。
 * <p>
 * 源码分析:
 * <a href="https://www.jianshu.com/p/46f333d496b5">简书</a>
 */
class SingletonPattern5 private constructor() {

  companion object {

    val instance: SingletonPattern5 by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {SingletonPattern5()}

  }

}
4.2.5 单例模式验证 Demo
public class SingletonPatternDemo {
  public static void main(String[] args) {
    // 获取唯一可用的对象
    SingletonPattern1 single1_1 = SingletonPattern1.getInstance();
    SingletonPattern1 single1_2 = SingletonPattern1.getInstance();
    // 输出打印结果
    System.out.println("Inside hashCode isTrue:" + (single1_1.hashCode() == single1_2.hashCode()));

    // 获取唯一可用的对象
    SingletonPattern2 single2_1 = SingletonPattern2.getInstance();
    SingletonPattern2 single2_2 = SingletonPattern2.getInstance();
    // 输出打印结果
    System.out.println("Inside hashCode isTrue:" + (single2_1.hashCode() == single2_2.hashCode()));

    // 获取唯一可用的对象
    SingletonPattern3 single3_1 = SingletonPattern3.Companion.getInstance();
    SingletonPattern3 single3_2 = SingletonPattern3.Companion.getInstance();
    // 输出打印结果
    System.out.println("Inside hashCode isTrue:" + (single3_1.hashCode() == single3_2.hashCode()));

    // 获取唯一可用的对象
    SingletonPattern4 single4_1 = SingletonPattern4.Companion.getInstance();
    SingletonPattern4 single4_2 = SingletonPattern4.Companion.getInstance();
    // 输出打印结果
    System.out.println("Inside hashCode isTrue:" + (single4_1.hashCode() == single4_2.hashCode()));

    // 获取唯一可用的对象
    SingletonPattern5 single5_1 = SingletonPattern5.Companion.getInstance();
    SingletonPattern5 single5_2 = SingletonPattern5.Companion.getInstance();
    // 输出打印结果
    System.out.println("Inside hashCode isTrue:" + (single5_1.hashCode() == single5_2.hashCode()));
  }
}

- 执行程序,输出结果:
```java
Inside Inside hashCode isTrue: true.
Inside Inside hashCode isTrue: true.
Inside Inside hashCode isTrue: true.
Inside Inside hashCode isTrue: true.
Inside Inside hashCode isTrue: true.

5 总结

  • 单例模式通过确保类只有一个实例,并提供一个全局访问点,适用于需要全局唯一实例的场景。虽然简单易用,但需要注意其潜在的缺点,如不易扩展和隐藏依赖。在实际应用中,需根据具体情况权衡使用单例模式带来的利弊。