Java 单例模式 (Singleton)

发布于:2025-02-15 ⋅ 阅读:(14) ⋅ 点赞:(0)

一、单例模式的定义 (Singleton Pattern Definition)

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一的实例。

核心思想:

  • 限制实例化: 防止外部通过 new 关键字创建类的多个实例。
  • 自我创建: 类自身负责创建自己的唯一实例。
  • 全局访问: 提供一个静态方法或静态变量,允许全局访问这个唯一实例。

二、单例模式的目的 (Purpose of Singleton Pattern)

单例模式的主要目的是:

  1. 控制实例数量: 确保一个类在任何情况下都只有一个实例存在。
  2. 节省资源: 避免创建多个相同的对象,节省内存空间和系统资源(例如,创建对象是一个耗时的操作)。
  3. 全局访问: 提供一个全局访问点,方便其他对象访问这个唯一实例,无需传递对象引用。
  4. 数据共享: 在多个模块或组件之间共享数据或状态。
  5. 协调行为: 协调系统中的不同部分的行为,例如,确保只有一个线程池、一个缓存管理器、一个配置对象等。

三、单例模式的实现方式 (Singleton Pattern Implementations)

单例模式有多种实现方式,每种方式都有其优缺点。 以下是常见的几种实现方式:

  1. 饿汉式 (Eager Initialization):

    • 原理: 在类加载时就立即创建单例实例。

    • 优点:

      • 简单,易于实现。
      • 线程安全(由 JVM 保证)。
      • 获取实例的速度快(无需延迟加载)。
    • 缺点:

      • 无论是否使用该实例,都会在类加载时创建,可能造成资源浪费(如果实例的创建比较耗时,或者实例很大)。
      • 无法实现延迟加载。
    • 代码示例:

      public class EagerSingleton {
          // 私有静态成员变量,在类加载时就创建实例
          private static final EagerSingleton instance = new EagerSingleton();
      
          // 私有构造方法,防止外部创建实例
          private EagerSingleton() {}
      
          // 公共静态方法,提供全局访问点
          public static EagerSingleton getInstance() {
              return instance;
          }
      }
      
  2. 懒汉式 (Lazy Initialization) - 线程不安全:

    • 原理: 延迟加载,只有在第一次使用时才创建单例实例。

    • 优点:

      • 避免了不必要的实例创建,节省资源。
      • 实现了延迟加载。
    • 缺点:

      • 线程不安全:在多线程环境下,可能会创建多个实例。
    • 代码示例:

      public class LazySingleton {
          private static LazySingleton instance;
      
          private LazySingleton() {}
      
          // 线程不安全
          public static LazySingleton getInstance() {
              if (instance == null) { // 如果实例不存在,则创建
                  instance = new LazySingleton();
              }
              return instance;
          }
      }
      
  3. 懒汉式 (Lazy Initialization) - 线程安全 (synchronized):

    • 原理:getInstance() 方法上加 synchronized 关键字,保证线程安全。

    • 优点:

      • 线程安全。
      • 实现了延迟加载。
    • 缺点:

      • 性能较低:每次调用 getInstance() 方法都需要进行同步,即使实例已经创建了。
    • 代码示例:

      public class LazySingletonSynchronized {
          private static LazySingletonSynchronized instance;
      
          private LazySingletonSynchronized() {}
      
          // 使用 synchronized 关键字保证线程安全
          public static synchronized LazySingletonSynchronized getInstance() {
              if (instance == null) {
                  instance = new LazySingletonSynchronized();
              }
              return instance;
          }
      }
      
  4. 双重检查锁定 (Double-Checked Locking) - 线程安全 (需要 volatile):

    • 原理:getInstance() 方法中,先检查实例是否已经创建,如果未创建,再进行同步和创建。

    • 优点:

      • 线程安全。
      • 实现了延迟加载。
      • 性能较好(相比于直接在 getInstance() 方法上加 synchronized)。
    • 缺点:

      • 代码稍微复杂一些。
      • 需要使用 volatile 关键字来禁止指令重排序(JDK 1.5 之后)。
    • 代码示例:

      public class DoubleCheckedLockingSingleton {
          // 使用 volatile 关键字禁止指令重排序
          private volatile static DoubleCheckedLockingSingleton instance;
      
          private DoubleCheckedLockingSingleton() {}
      
          public static DoubleCheckedLockingSingleton getInstance() {
              if (instance == null) { // 第一次检查
                  synchronized (DoubleCheckedLockingSingleton.class) { // 加锁
                      if (instance == null) { // 第二次检查
                          instance = new DoubleCheckedLockingSingleton();
                      }
                  }
              }
              return instance;
          }
      }
      
    • 为什么要使用 volatile 关键字?
      * instance = new DoubleCheckedLockingSingleton(); 这行代码不是原子操作,它实际上包含了三个步骤:
      1. 分配内存空间。
      2. 初始化对象。
      3. 将 instance 指向分配的内存空间。

      • 如果没有 volatile 关键字,可能会发生指令重排序,导致线程 A 在对象初始化完成之前就将 instance 指向了分配的内存空间,此时线程 B 访问 instance,会发现 instance 不为 null,但实际上对象还没有初始化完成,导致错误。
      • volatile 关键字可以禁止指令重排序,保证这三个步骤的顺序性。
  5. 静态内部类 (Static Inner Class) - 线程安全 (推荐)

    • 原理: 利用 Java 类加载机制来实现延迟加载和线程安全。 静态内部类只有在被使用时才会被加载。

    • 优点:

      • 线程安全(由 JVM 保证)。
      • 实现了延迟加载。
      • 代码简洁。
      • 性能较好。
    • 推荐: 这是实现单例模式的一种推荐方式。

    • 代码示例:

      public class StaticInnerClassSingleton {
      
          private StaticInnerClassSingleton() {}
      
          // 静态内部类
          private static class SingletonHolder {
              private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
          }
      
          public static StaticInnerClassSingleton getInstance() {
              return SingletonHolder.INSTANCE; // 访问静态内部类的静态成员变量
          }
      }
      
  6. 枚举 (Enum) - 线程安全 (最佳实践)

    • 原理: Java 枚举类型本身就是单例的,JVM 保证枚举实例的唯一性。

    • 优点:

      • 线程安全(由 JVM 保证)。
      • 代码简洁。
      • 防止反射攻击和序列化破坏单例。
      • 最佳实践。
    • 推荐: 《Effective Java》作者 Josh Bloch 推荐使用枚举来实现单例模式。

    • 代码示例:

      public enum EnumSingleton {
          INSTANCE; // 唯一的枚举实例
      
          // 可以添加其他方法和属性
          public void doSomething() {
              System.out.println("EnumSingleton is doing something...");
          }
      }
      
      // 使用
      EnumSingleton.INSTANCE.doSomething();
      

四、单例模式的优缺点

  • 优点:

    • 控制实例数量,节省资源。
    • 提供全局访问点,方便使用。
    • 可以实现延迟加载 (懒汉式)。
    • 可以避免多线程并发访问同一个资源造成的冲突。
  • 缺点:

    • 违反单一职责原则: 单例类既负责创建和管理自身实例,又负责业务逻辑,职责不够单一。
    • 扩展困难: 如果需要创建多个实例,或者需要对单例类进行扩展,会比较困难。
    • 隐藏依赖关系: 单例模式的全局访问点可能会隐藏类之间的依赖关系,使代码难以理解和测试。
    • 可能导致内存泄漏: 如果单例对象持有对外部资源的引用,并且这些资源没有被及时释放,可能会导致内存泄漏。
    • 在分布式系统中难以实现真正的单例: 在分布式环境中,每个 JVM 都会创建一个单例实例,除非使用分布式锁或其他机制来保证全局唯一性。

五、单例模式的应用场景

  1. 需要频繁创建和销毁的对象: 如果对象的创建和销毁开销较大,可以使用单例模式来避免频繁创建和销毁对象,例如:

    • 线程池 (ThreadPoolExecutor): 管理线程资源,避免频繁创建和销毁线程。
    • 缓存 (Cache): 缓存数据,避免频繁从数据库或其他地方加载数据。
    • 数据库连接池 (Connection Pool): 管理数据库连接,避免频繁创建和关闭连接。
    • 配置加载类: 加载和管理配置信息
  2. 需要全局唯一或共享的对象:

    • 配置对象 (Configuration Object): 应用程序的配置信息通常只需要一个实例。
    • 日志记录器 (Logger): 应用程序的日志记录器通常只需要一个实例。
    • 计数器 (Counter): 全局唯一的计数器。
    • ID 生成器 (ID Generator): 全局唯一的 ID 生成器。
    • 应用程序上下文 (Application Context): 例如 Spring 中的 ApplicationContext
    • ServletContext: 在 Java Web 应用中,每个 Web 应用只有一个 ServletContext 对象。
    • Windows 系统的任务管理器,回收站 等。
  3. 控制资源访问:

    • 打印机 (Printer): 确保同一时间只有一个任务可以访问打印机。
    • 文件系统 (File System): 确保对同一个文件的访问是互斥的。
    • 硬件设备: 例如串口、显卡等,通常需要单例模式来控制访问。
  4. Spring Bean的默认作用域:

    • Spring Bean 的默认作用域就是 singleton

六、单例模式与其他设计模式的关系

  • 工厂模式 (Factory Pattern): 工厂模式可以用来创建单例对象。
  • 抽象工厂模式 (Abstract Factory Pattern): 抽象工厂模式也可以用来创建单例对象。
  • 建造者模式 (Builder Pattern): 可以用来构建复杂对象的单例。
  • 原型模式 (Prototype Pattern): 原型模式与单例模式冲突,原型模式要求每次都返回一个新对象。
  • 外观模式 (Facade Pattern): 外观类通常被设计为单例。

七、最佳实践和注意事项

  1. 选择合适的实现方式: 根据你的需求和场景选择最合适的单例模式实现方式。 推荐使用枚举静态内部类方式。
  2. 线程安全: 确保你的单例模式实现是线程安全的。
  3. 延迟加载: 如果单例对象的创建比较耗时,或者不一定会被使用,可以考虑使用延迟加载。
  4. 防止反射攻击: 可以通过在私有构造方法中进行判断,防止通过反射创建多个实例。
 private Singleton() {
     if (instance != null) {
         throw new IllegalStateException("Already instantiated");
     }
 }
  1. 防止序列化破坏单例: 如果单例类实现了 Serializable 接口,需要重写 readResolve() 方法,返回单例实例,防止反序列化创建多个实例。
      // readResolve to prevent another instance of Singleton
    private Object readResolve() {
        return instance; // 返回已存在的单例实例
    }
    
  2. 谨慎使用单例模式: 不要滥用单例模式。 单例模式会增加代码的耦合度,降低可测试性。 只有在确实需要全局唯一实例时才使用单例模式。
  3. 考虑依赖注入: 在 Spring 等框架中,可以使用依赖注入来管理单例对象,而不是自己实现单例模式。
  4. 分布式环境: 在分布式环境中,需要考虑如何实现全局唯一的单例,可以使用分布式锁、分布式配置中心等技术。
  5. 测试: 对单例类进行单元测试,确保其功能正确,并且只有一个实例被创建。

总结

单例模式是一种常用的设计模式,它可以确保一个类只有一个实例,并提供全局访问点。 单例模式有多种实现方式,每种方式都有其优缺点。 在使用单例模式时,需要考虑线程安全、延迟加载、反射攻击、序列化、以及分布式环境等问题。