单例模式:高效实现全局唯一实例

单例模式(Singleton Pattern):确保一个类仅有一个实例

单例模式是 创建型设计模式 的核心之一,核心目标是:一个类在整个应用生命周期中,只允许创建一个实例对象,并提供全局唯一的访问入口。

它广泛应用于需要共享资源、避免重复创建开销的场景(如配置管理、数据库连接池、日志工具、Spring 的默认 Bean 等)。

一、单例模式的核心原则

  1. 唯一实例 :类的构造器必须私有化(private),禁止外部通过 new 关键字创建实例;
  2. 全局访问 :提供公开的静态方法(如 getInstance()),作为获取唯一实例的唯一入口;
  3. 线程安全 :多线程环境下,需避免并发调用 getInstance() 时创建多个实例;
  4. 懒加载 / 饿加载:根据实例创建时机,分为 "需要时才创建"(懒加载)和 "类加载时就创建"(饿加载)。

二、单例模式的常见实现方式(Java)

1. 饿汉式(Eager Initialization):类加载时创建实例

核心思路:

类加载阶段(JVM 加载类时)就初始化唯一实例,依赖 JVM 的类加载机制保证线程安全(JVM 对类加载是 "单线程" 的,不会并发初始化)。

实现代码:

java

运行

复制代码
public class EagerSingleton {
    // 1. 私有静态实例(类加载时直接初始化,饿汉="饿了马上吃")
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    // 2. 私有化构造器:禁止外部new
    private EagerSingleton() {}

    // 3. 公开静态方法:返回唯一实例
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }

    // 示例:单例的业务方法
    public void doSomething() {
        System.out.println("饿汉式单例:" + this.hashCode());
    }
}
优缺点:
  • 优点:实现简单、天然线程安全(依赖 JVM 类加载机制)、无锁开销,获取实例速度快;
  • 缺点:懒加载失效(类加载时就创建实例),如果实例从未被使用,会造成内存浪费(如重量级对象:数据库连接池)。
适用场景:

实例占用内存小、确定会被使用,或需要提前初始化的场景(如系统核心配置)。

2. 懒汉式(Lazy Initialization):需要时才创建实例

核心思路:

类加载时不创建实例,第一次调用 getInstance() 时才初始化,实现 "懒加载"(节省内存)。注意:默认非线程安全,需手动处理并发问题。

(1)基础懒汉式(非线程安全,禁止使用)

java

运行

复制代码
public class LazySingletonUnsafe {
    // 1. 私有静态实例(仅声明,不初始化)
    private static LazySingletonUnsafe instance;

    // 2. 私有化构造器
    private LazySingletonUnsafe() {}

    // 3. 公开静态方法:第一次调用时创建实例
    public static LazySingletonUnsafe getInstance() {
        if (instance == null) { // 多线程下,多个线程可能同时进入这里
            instance = new LazySingletonUnsafe(); // 导致创建多个实例
        }
        return instance;
    }
}
  • 问题 :多线程并发调用 getInstance() 时,多个线程会同时通过 instance == null 判断,进而创建多个实例,破坏单例原则。
(2)线程安全的懒汉式(加锁 synchronized,低效)

java

运行

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

    private LazySingletonSynchronized() {}

    // 给整个方法加锁:保证同一时间只有一个线程能进入
    public static synchronized LazySingletonSynchronized getInstance() {
        if (instance == null) {
            instance = new LazySingletonSynchronized();
        }
        return instance;
    }
}
  • 优点:解决线程安全问题;
  • 缺点:锁粒度太大(整个方法加锁),即使实例已创建,后续所有线程获取实例时仍需排队等待,并发效率极低。
(3)双重检查锁定(Double-Check Locking,DCL):高效线程安全
核心优化:
  • 第一次检查 instance == null:避免实例已创建时的锁竞争;
  • 第二次检查 instance == null:防止多个线程同时通过第一次检查后,重复创建实例;
  • volatile 关键字:禁止指令重排序(关键!避免 "半初始化实例" 问题)。
实现代码(最终版):

java

运行

复制代码
public class LazySingletonDCL {
    // 1. 私有静态实例 + volatile 修饰(禁止指令重排序)
    private static volatile LazySingletonDCL instance;

    // 2. 私有化构造器
    private LazySingletonDCL() {}

    // 3. 双重检查锁定:高效线程安全
    public static LazySingletonDCL getInstance() {
        // 第一次检查:实例已存在则直接返回,避免锁竞争
        if (instance == null) {
            synchronized (LazySingletonDCL.class) { // 类锁:锁定整个类
                // 第二次检查:防止多个线程同时进入第一次检查后,重复创建
                if (instance == null) {
                    instance = new LazySingletonDCL(); 
                    // 注意:new操作非原子性,volatile避免指令重排序导致的"半初始化"
                }
            }
        }
        return instance;
    }

    public void doSomething() {
        System.out.println("DCL懒汉式单例:" + this.hashCode());
    }
}
关键说明:volatile 的作用

new LazySingletonDCL() 实际分为 3 步:

  1. 分配内存空间;
  2. 初始化实例(调用构造器);
  3. 将实例引用指向内存空间。

JVM 可能会对指令重排序(如 1→3→2),导致线程 A 执行到 3 时,线程 B 看到 instance != null,但实例未完成初始化(半初始化状态),进而报错。volatile 关键字会禁止指令重排序,保证 1→2→3 的执行顺序,避免该问题。

优缺点:
  • 优点:懒加载(节省内存)、线程安全、并发效率高(仅初始化时加锁,后续无锁);
  • 缺点 :实现稍复杂,需理解 volatile 和双重检查的逻辑。
适用场景:

实例占用内存大、不确定是否会被使用,且需要高并发访问的场景(推荐首选)。

3. 静态内部类(Static Inner Class):完美结合懒加载与线程安全

核心思路:

利用 Java 的 "静态内部类加载机制":静态内部类不会随外部类加载而初始化,只有第一次调用其静态成员时才会加载,且加载过程线程安全。

实现代码:

java

运行

复制代码
public class StaticInnerClassSingleton {
    // 1. 私有静态内部类:存储唯一实例
    private static class SingletonHolder {
        // 内部类加载时初始化实例(线程安全)
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    // 2. 私有化构造器
    private StaticInnerClassSingleton() {}

    // 3. 公开静态方法:触发内部类加载,返回实例
    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public void doSomething() {
        System.out.println("静态内部类单例:" + this.hashCode());
    }
}
原理:
  • 外部类 StaticInnerClassSingleton 加载时,内部类 SingletonHolder 不会加载,实现懒加载;
  • 第一次调用 getInstance() 时,触发 SingletonHolder 加载,初始化 INSTANCE,JVM 保证该过程线程安全;
  • 后续调用直接返回已初始化的实例,无锁开销。
优缺点:
  • 优点:懒加载、线程安全、实现简单、无锁高效(集合了饿汉式和 DCL 的优点);
  • 缺点:无法传参初始化(如果实例需要依赖外部参数,该方式不适用)。
适用场景:

无需传参初始化的单例(推荐首选,代码简洁且无潜在问题)。

4. 枚举单例(Enum Singleton):终极解决方案

核心思路:

利用 Java 枚举(Enum)的天然特性:

  • 枚举类的实例是天然单例(JVM 保证);
  • 枚举类的构造器默认是私有(无法通过 new 创建);
  • 支持序列化 / 反序列化安全(避免反射破坏单例)。
实现代码:

java

运行

复制代码
public enum EnumSingleton {
    // 唯一实例(枚举常量即为单例实例)
    INSTANCE;

    // 单例的业务方法(直接在枚举中定义)
    public void doSomething() {
        System.out.println("枚举单例:" + this.hashCode());
    }

    // (可选)如果需要复杂初始化,可添加构造器
    EnumSingleton() {
        // 初始化逻辑(如加载配置、连接资源)
        System.out.println("枚举单例初始化");
    }
}
使用方式:

java

运行

复制代码
// 直接通过枚举常量获取实例
EnumSingleton singleton = EnumSingleton.INSTANCE;
singleton.doSomething();
核心优势(为什么是 "终极方案"):
  1. 天然线程安全:JVM 保证枚举实例的初始化是单线程的,无需手动加锁;
  2. 防止反射破坏 :Java 反射机制无法创建枚举类的实例(会抛出 IllegalArgumentException);
  3. 序列化安全 :普通单例序列化后反序列化会创建新实例,枚举类默认重写了 readResolve() 方法,保证反序列化后仍是原实例;
  4. 实现极简:一行代码定义实例,无需处理懒加载、线程安全等细节。
缺点:
  • 懒加载失效(枚举类加载时就初始化实例,与饿汉式类似);
  • 枚举实例默认是单例,无法动态创建多个实例(符合单例原则,非真正缺点)。
适用场景:

需要绝对安全(防止反射、序列化破坏)的单例场景(如核心配置、安全组件),是《Effective Java》作者推荐的最优单例实现。

三、单例模式的常见问题与解决方案

1. 反射破坏单例

问题:

普通单例(如饿汉式、DCL)的私有构造器,可通过 Java 反射机制强制调用,创建新实例:

java

运行

复制代码
// 反射破坏饿汉式单例
Class<?> clazz = EagerSingleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true); // 跳过访问检查
EagerSingleton instance1 = EagerSingleton.getInstance();
EagerSingleton instance2 = (EagerSingleton) constructor.newInstance();
System.out.println(instance1 == instance2); // false(破坏单例)
解决方案:
  • 方案 1:使用枚举单例(天然防止反射);

  • 方案 2:在私有构造器中添加校验,禁止重复创建: java

    运行

    复制代码
    private EagerSingleton() {
        // 防止反射破坏:如果实例已存在,抛出异常
        if (INSTANCE != null) {
            throw new IllegalStateException("单例实例已存在,禁止重复创建");
        }
    }

2. 序列化 / 反序列化破坏单例

问题:

普通单例实现 Serializable 接口后,序列化后再反序列化会创建新实例:

java

运行

复制代码
// 序列化普通单例
EagerSingleton instance1 = EagerSingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
oos.writeObject(instance1);

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
EagerSingleton instance2 = (EagerSingleton) ois.readObject();
System.out.println(instance1 == instance2); // false(破坏单例)
解决方案:
  • 方案 1:使用枚举单例(天然支持序列化安全);

  • 方案 2:在普通单例中重写 readResolve() 方法,返回已有的单例实例:

    java

    运行

    复制代码
    public class EagerSingleton implements Serializable {
        private static final EagerSingleton INSTANCE = new EagerSingleton();
    
        private EagerSingleton() {}
    
        public static EagerSingleton getInstance() {
            return INSTANCE;
        }
    
        // 反序列化时调用,返回已有实例
        private Object readResolve() {
            return INSTANCE;
        }
    }

3. 多 ClassLoader 环境下的单例问题

问题:

如果应用中有多个类加载器(如 Tomcat 的 WebAppClassLoader),每个类加载器会单独加载单例类,导致每个类加载器下都有一个单例实例(破坏 "全局唯一")。

解决方案:
  • 让单例类由 "父类加载器" 加载(如 JVM 的 Bootstrap ClassLoader);
  • getInstance() 中指定类加载器,确保全局使用同一个类加载器。

四、单例模式的应用场景

  1. 资源共享类 :如日志工具(Logger)、配置管理器(Config)、线程池、数据库连接池(DataSource);
  2. 重量级对象:创建成本高的对象(如网络连接、大型缓存),避免重复创建开销;
  3. 全局状态管理:需要统一维护状态的对象(如计数器、全局上下文);
  4. 框架默认实现 :Spring 容器中的 Bean 默认是单例(singleton 作用域),通过 JavaConfig 或 XML 配置管理。

五、单例模式的优缺点总结

优点:

  1. 减少内存开销:仅创建一个实例,避免重复创建重量级对象;
  2. 统一访问控制:全局唯一入口,便于维护对象状态和资源共享;
  3. 避免资源冲突:如数据库连接池避免多线程并发创建过多连接导致的资源耗尽。

缺点:

  1. 违背单一职责原则:单例类既要负责自身业务逻辑,又要负责实例创建和管理;
  2. 扩展性差:单例类通常没有接口,无法通过继承扩展(除非修改原代码);
  3. 测试困难:单例类依赖全局状态,难以进行单元测试(无法模拟不同实例的场景);
  4. 可能引发内存泄漏:单例实例生命周期与应用一致,若持有外部对象引用且未释放,会导致内存泄漏。

六、常见实现方式对比表

实现方式 懒加载 线程安全 防止反射 序列化安全 实现复杂度 推荐度
饿汉式 简单 ⭐⭐⭐
基础懒汉式(非线程安全) 简单
同步方法懒汉式 简单 ⭐⭐
双重检查锁定(DCL) 中等 ⭐⭐⭐⭐
静态内部类 简单 ⭐⭐⭐⭐⭐
枚举单例 极简 ⭐⭐⭐⭐⭐

总结

  • 若需 懒加载 + 无复杂需求:首选「静态内部类」(简单高效);
  • 若需 绝对安全(防反射 / 序列化):首选「枚举单例」(终极方案);
  • 若需 传参初始化:选择「双重检查锁定(DCL)」;
  • 若实例 占用内存小、确定会使用:选择「饿汉式」(简单直接);
  • 避免使用「基础懒汉式」和「同步方法懒汉式」(前者线程不安全,后者并发低效)。

单例模式是最常用的设计模式之一,但需注意避免滥用(如无需全局唯一的场景强行使用,会导致扩展性和测试性问题)。

相关推荐
xunyan62341 小时前
面向对象(下)-设计模式与单例设计模式
java·单例模式·设计模式
stormsha14 小时前
Java 设计模式探秘饿汉式与懒汉式单例模式的深度解析
java·单例模式·设计模式·java-ee
口袋物联20 小时前
设计模式之单例模式在 C 语言中的应用(含 Linux 内核实例)
c语言·单例模式·设计模式
__万波__20 小时前
二十三种设计模式(一)--单例模式
java·单例模式·设计模式
第二只羽毛1 天前
单例模式的初识
java·大数据·数据仓库·单例模式
极地星光2 天前
Qt/C++ 单例模式深度解析:饿汉式与懒汉式实战指南
c++·qt·单例模式
Mr.Winter`5 天前
基于Proto3和单例模式的系统参数配置模块设计(附C++案例实现)
c++·人工智能·单例模式·机器人
专注于大数据技术栈6 天前
java学习--单例模式之懒汉式
java·学习·单例模式
Murphy_lx6 天前
单例模式_
单例模式