单例模式介绍

1. 什么是单例模式?

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

你可以把它想象成一个国家的国王或一个公司的CEO。在任何时候,都只能有一个人担任这个角色,所有人都通过统一的渠道(如朝廷、董事会)来向他/她汇报或请求指示。

主要目的:

  • 控制资源访问:确保某个资源(如数据库连接池、线程池、配置管理器)在整个应用程序中只有一个实例,避免资源浪费和冲突。
  • 保证数据一致性:当多个地方需要共享和操作同一份数据时,单例可以确保数据的一致性。
  • 方便管理:提供一个统一的入口来访问该实例,便于集中管理和控制。

2. 单例模式的 UML 类图

单例模式的结构非常简单,通常只包含一个角色:单例类

复制代码
+---------------------+
|     Singleton       |
+---------------------+
| - instance: Singleton|  // 私有的静态实例
+---------------------+
| - Singleton()       |  // 私有的构造函数
| + getInstance():    |  // 公有的静态获取实例方法
|      Singleton      |
+---------------------+

关键要素:

  1. 私有静态成员变量 (instance):用于存储单例类的唯一实例。由于是静态的,它属于类本身,而不是类的某个对象。
  2. 私有构造函数 (Singleton()) :这是实现单例的"防火墙"。将构造函数设为私有,可以防止外部代码通过 new Singleton() 的方式创建新的实例。
  3. 公有静态方法 (getInstance()):这是全局访问点。外部代码通过调用这个方法来获取单例实例。该方法负责控制实例的创建过程,确保只创建一次。

3. 单例模式的多种实现方式(Java 示例)

单例模式有多种实现方式,每种方式在线程安全性能实现复杂度上各有优劣。

方式一:饿汉式

思想:在类加载时就立即创建实例,就像一个"饿汉"一样,不管你需不需要,我先创建好再说。

复制代码
public class EagerSingleton {
    // 1. 私有静态成员变量,在类加载时就初始化
    private static final EagerSingleton instance = new EagerSingleton();

    // 2. 私有构造函数,防止外部 new
    private EagerSingleton() {
        // 防止通过反射攻击
        if (instance != null) {
            throw new IllegalStateException("Singleton already initialized");
        }
    }

    // 3. 公有静态方法,直接返回已创建好的实例
    public static EagerSingleton getInstance() {
        return instance;
    }
}
  • 优点
    • 实现简单:代码最简洁。
    • 线程安全 :由于实例是在类加载时创建的,由 JVM 保证了其线程安全性。static final 关键字确保了实例的唯一性和不可变性。
  • 缺点
    • 可能造成资源浪费:如果这个单例对象非常大,并且在整个应用程序运行期间都没有被使用,那么它就会一直占用内存,造成不必要的资源开销。
方式二:懒汉式(线程不安全)

思想 :延迟加载,只有在第一次调用 getInstance() 方法时才创建实例。

复制代码
public class LazySingleton {
    // 1. 私有静态成员变量,先不初始化
    private static LazySingleton instance;

    // 2. 私有构造函数
    private LazySingleton() {}

    // 3. 公有静态方法,获取实例时才创建
    public static LazySingleton getInstance() {
        if (instance == null) { // 第一次检查
            instance = new LazySingleton();
        }
        return instance;
    }
}
  • 优点
    • 延迟加载:避免了饿汉式可能造成的资源浪费,实现了按需创建。
  • 缺点
    • 线程不安全 :在多线程环境下,如果多个线程同时通过 if (instance == null) 检查,并且都发现 instancenull,那么它们就会各自创建一个实例,从而破坏了单例模式。这种方式在生产环境中绝对不能使用!
方式三:懒汉式(同步方法,线程安全)

思想 :为了解决方式二的线程安全问题,在 getInstance() 方法上加上 synchronized 关键字,使其成为同步方法。

复制代码
public class SynchronizedLazySingleton {
    private static SynchronizedLazySingleton instance;

    private SynchronizedLazySingleton() {}

    // 使用 synchronized 关键字修饰方法
    public static synchronized SynchronizedLazySingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedLazySingleton();
        }
        return instance;
    }
}
  • 优点
    • 线程安全synchronized 确保了同一时间只有一个线程能进入该方法,保证了实例的唯一性。
    • 延迟加载:保留了懒汉式的优点。
  • 缺点
    • 性能低下 :每次调用 getInstance() 方法都会进行同步,即使实例已经被创建。同步操作会带来性能开销,而这个锁只有在第一次创建实例时才是必要的。之后每次获取实例都需要等待锁,效率很低。
方式四:双重检查锁定 - 推荐

思想:这是对方式三的优化。我们只在第一次创建实例时进行同步,后续获取实例时无需同步,从而兼顾了线程安全和性能。

复制代码
public class DoubleCheckedLockingSingleton {
    // 使用 volatile 关键字修饰,防止指令重排序
    private static volatile 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 Singleton() 这行代码并非原子操作,它大致分为三步:

    1. memory = allocate(); // 分配对象的内存空间
    2. ctorInstance(memory); // 调用构造函数,初始化对象
    3. instance = memory; // 将 instance 指向分配的内存地址

    由于 JVM 的指令重排序优化,步骤 2 和 3 的顺序可能颠倒。如果线程 A 执行完 1 和 3,但还没执行 2,此时 instance 已经不为 null。线程 B 在第一次检查时发现 instance 不为 null,就会直接返回一个尚未初始化完成 的实例,导致程序出错。volatile 关键字可以禁止指令重排序,确保 2 和 3 按顺序执行。

  • 优点

    • 线程安全:DCL 机制保证了多线程环境下的正确性。
    • 延迟加载:保留了懒加载的优点。
    • 性能高:只有在第一次创建实例时才需要同步,后续调用无需加锁,性能几乎与饿汉式相当。
  • 缺点

    • 实现稍复杂 :需要理解 volatile 和 DCL 的原理。
方式五:静态内部类 - 强烈推荐

思想:利用 Java 的类加载机制来保证线程安全和延迟加载。这是目前公认的最佳实现方式之一。

复制代码
public class StaticInnerClassSingleton {
    // 私有构造函数
    private StaticInnerClassSingleton() {}

    // 静态内部类
    private static class SingletonHolder {
        // 静态内部类中的静态成员变量
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    // 公有静态方法,返回内部类的实例
    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 工作原理

    1. 延迟加载StaticInnerClassSingleton 类被加载时,其静态内部类 SingletonHolder 并不会 被立即加载。只有当第一次调用 getInstance() 方法时,SingletonHolder 类才会被加载。
    2. 线程安全 :JVM 在加载类时,会保证其静态成员变量的初始化是线程安全的。因此,INSTANCE 的创建过程由 JVM 保证,不会有线程安全问题。
  • 优点

    • 线程安全:由 JVM 保证,无需任何同步代码。
    • 延迟加载:完美实现了按需加载。
    • 实现简单 :代码比 DCL 更简洁,没有 volatile 和同步块,可读性高。
    • 性能高:没有同步开销。
方式六:枚举

思想:利用 Java 枚举类型的特性来实现单例。这是《Effective Java》作者 Josh Bloch 极力推荐的方式。

复制代码
public enum EnumSingleton {
    // 这个枚举元素本身就是单例
    INSTANCE;

    // 可以添加你需要的任何方法
    public void doSomething() {
        System.out.println("Singleton is doing something.");
    }
}

// 如何使用
public class Main {
    public static void main(String[] args) {
        EnumSingleton singleton = EnumSingleton.INSTANCE;
        singleton.doSomething();
    }
}
  • 优点
    • 绝对线程安全:枚举的实现同样由 JVM 保证。
    • 防止反射攻击:普通单例可以通过反射调用私有构造函数来创建新实例,但枚举不行,JVM 会阻止这种操作。
    • 防止序列化/反序列化破坏单例:普通单例在序列化再反序列化后,会创建一个新对象。而枚举在序列化时,JVM 只是保证了枚举实例的唯一性。
    • 代码极其简洁:是所有实现方式中最简单的。
  • 缺点
    • 不够灵活:由于枚举的特性,这种方式不太适用于需要继承父类或实现特定接口的场景(虽然可以实现接口,但感觉上有些奇怪)。对于大多数场景,这不是问题。

4. 单例模式的优缺点

优点
  1. 内存占用少:只有一个实例,避免了频繁创建和销毁对象带来的性能开销。
  2. 访问方便:提供了全局唯一的访问点,方便统一管理。
  3. 资源共享:适合需要共享资源的场景,如配置文件、日志工具、数据库连接池等。
缺点
  1. 扩展性差:由于构造函数私有,单例类通常不能被继承,这限制了它的扩展性。
  2. 职责过重:单例模式既承担了业务逻辑,又承担了对象创建管理的职责,在一定程度上违背了"单一职责原则"。
  3. 测试困难:在单元测试中,如果单例对象持有状态,那么测试之间可能会相互影响,因为单例是全局共享的。需要特别注意测试的隔离性。
  4. 滥用风险:因为使用方便,容易被滥用。在不适合的场景下使用单例,可能会导致程序结构混乱、耦合度增高。

5. 单例模式的适用场景

  • 需要频繁实例化然后销毁的对象:如线程池、缓存、日志对象等。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象:如数据库连接池。
  • 工具类对象:如配置文件管理器、JSON/Xml 解析工具等。
  • 需要频繁访问数据库或文件的对象:如数据访问层(DAO)的实例。
  • 系统中只需要一个对象来协调行为:如 Windows 的任务管理器、回收站。

6. 最佳实践与总结

实现方式 线程安全 延迟加载 性能 推荐度 备注
饿汉式 ⭐⭐⭐⭐ 简单,但可能浪费资源
懒汉式(不安全) 绝对禁止使用
懒汉式(同步方法) ⭐⭐ 性能差,不推荐
双重检查锁定 ⭐⭐⭐⭐⭐ 强烈推荐,兼顾了所有优点
静态内部类 ⭐⭐⭐⭐⭐ 强烈推荐,代码更优雅
枚举 ⭐⭐⭐⭐⭐ 最佳实现,最安全,但不够灵活

如何选择?

  • 如果单例对象占用资源不大,且希望实现简单饿汉式是一个不错的选择。
  • 如果需要延迟加载,并且对代码简洁性要求高静态内部类是你的首选。它在绝大多数情况下都是完美的解决方案。
  • 如果需要延迟加载,并且单例类需要继承或实现复杂的逻辑双重检查锁定是最佳选择。
  • 如果对安全性要求极高,且不介意使用枚举枚举是理论上最完美的实现方式,能抵御反射和序列化攻击。

在现代 Java 开发中,静态内部类双重检查锁定是最常用和最受推崇的实现方式。选择哪种取决于你的具体需求和偏好。