目录

一、单例模式的核心要素

二、常见实现方式及优缺点(附代码)

1. 饿汉式(Eager Initialization):“饿了就马上吃”

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

3. 双重检查锁(Double-Checked Locking):“两次检查,兼顾安全与性能”

4. 静态内部类(Static Inner Class):“懒加载 + 天然线程安全”

5. 枚举(Enum):“天然防反射、防序列化”

三、单例模式的 “破坏者” 与解决方案

1. 反射攻击及防御

2. 反序列化攻击及防御

四、单例模式的应用场景

五、单例模式的优缺点

总结


单例模式(Singleton Pattern)是 Java 设计模式中创建型模式的一种,核心目标是保证一个类在整个应用中只有一个实例,并提供一个全局唯一的访问点。它的设计思想是 “控制实例数量,避免资源浪费”,广泛应用于全局配置、工具类、线程池等场景。

一、单例模式的核心要素

要实现单例模式,必须满足三个关键条件:

  1. 私有构造器(Private Constructor):禁止外部通过 new 关键字创建实例(切断外部实例化的途径)。
  2. 静态私有实例(Static Private Instance):在类内部创建唯一实例,并用 private static 修饰(确保全局只有一个实例,且属于类级别的资源)。
  3. 静态公有访问方法(Static Public Access Method):提供一个全局访问点(如 getInstance()),返回内部创建的唯一实例。

二、常见实现方式及优缺点(附代码)

单例模式有多种实现方式,核心区别在于实例创建时机线程安全处理,不同场景下需选择合适的方式。

1. 饿汉式(Eager Initialization):“饿了就马上吃”
  • 原理:类加载时就创建实例(“饿” 表示急切,不延迟)。

  • 代码实现

    java

    运行

    public class EagerSingleton {
        // 1. 私有构造器:禁止外部实例化
        private EagerSingleton() {}
    
        // 2. 静态私有实例:类加载时就初始化(唯一实例)
        private static final EagerSingleton INSTANCE = new EagerSingleton();
    
        // 3. 静态公有方法:提供全局访问点
        public static EagerSingleton getInstance() {
            return INSTANCE;
        }
    }
    
  • 优点

    • 实现简单,无需考虑线程安全(类加载由 JVM 保证线程安全,同一类只会加载一次)。
  • 缺点

    • 不支持延迟加载(类加载时就创建实例),如果实例初始化成本高(如占用大量内存),但程序全程未使用,会造成资源浪费。
  • 适用场景:实例初始化成本低,且肯定会被使用(如基础工具类)。

2. 懒汉式(Lazy Initialization):“需要时才创建”
  • 原理:第一次调用 getInstance() 时才创建实例(“懒” 表示延迟,节省资源)。

  • 基础版(线程不安全)

    java

    运行

    public class LazySingleton {
        private LazySingleton() {}
    
        // 不初始化,等第一次调用时再创建
        private static LazySingleton instance;
    
        public static LazySingleton getInstance() {
            if (instance == null) { // 问题:多线程下可能同时进入此处,创建多个实例
                instance = new LazySingleton();
            }
            return instance;
        }
    }
    

    问题:多线程并发调用 getInstance() 时,可能有多个线程同时判断 instance == null,导致创建多个实例(破坏单例)。

  • 线程安全版(同步方法)

    java

    运行

    public class LazySingleton {
        private LazySingleton() {}
    
        private static LazySingleton instance;
    
        // 加 synchronized 保证线程安全
        public static synchronized LazySingleton getInstance() {
            if (instance == null) {
                instance = new LazySingleton();
            }
            return instance;
        }
    }
    

    优点:解决线程安全问题(同步方法保证同一时间只有一个线程执行)。缺点:性能差(每次调用 getInstance() 都要加锁,而锁竞争会消耗资源,实际上只有第一次初始化需要锁)。

3. 双重检查锁(Double-Checked Locking):“两次检查,兼顾安全与性能”
  • 原理:通过 “两次判断实例是否为空”+“volatile 关键字”,既保证线程安全,又减少锁竞争(只有第一次初始化时加锁)。

  • 代码实现

    java

    运行

    public class DoubleCheckedLockingSingleton {
        private DoubleCheckedLockingSingleton() {}
    
        // volatile 关键:防止指令重排序(核心!)
        private static volatile DoubleCheckedLockingSingleton instance;
    
        public static DoubleCheckedLockingSingleton getInstance() {
            // 第一次检查:如果实例已存在,直接返回(无需加锁,提升性能)
            if (instance == null) {
                synchronized (DoubleCheckedLockingSingleton.class) {
                    // 第二次检查:进入同步块后再判断,防止多线程同时通过第一次检查
                    if (instance == null) {
                        instance = new DoubleCheckedLockingSingleton();
                    }
                }
            }
            return instance;
        }
    }
    
  • 为什么需要 volatile?instance = new DoubleCheckedLockingSingleton() 不是原子操作,会被拆分为 3 步:

    1. 分配内存空间;
    2. 初始化实例;
    3. 将 instance 指向内存空间。若没有 volatile,JVM 可能对步骤 2 和 3 重排序(先执行 3 再执行 2),导致线程 A 未完成初始化时,线程 B 看到 instance != null 并直接使用,引发空指针异常。volatile 可禁止指令重排序,保证安全性。
  • 优点:线程安全,且只有第一次初始化时加锁,性能优秀(推荐用于高并发场景)。

4. 静态内部类(Static Inner Class):“懒加载 + 天然线程安全”
  • 原理:利用 JVM 类加载机制,通过静态内部类延迟初始化实例(内部类只有在被使用时才会加载)。

  • 代码实现

    java

    运行

    public class StaticInnerClassSingleton {
        private StaticInnerClassSingleton() {}
    
        // 静态内部类:只在被调用时才加载
        private static class SingletonHolder {
            // 内部类中创建实例,由 JVM 保证线程安全(类加载过程是线程安全的)
            private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
        }
    
        // 外部类调用时,才会加载内部类并初始化实例
        public static StaticInnerClassSingleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    
  • 优点

    • 懒加载:内部类不被调用就不会加载,节省资源;
    • 线程安全:JVM 保证类加载过程的线程安全,无需额外同步;
    • 性能好:没有锁竞争,比双重检查锁更简洁。
  • 缺点:无法通过反射或反序列化破坏单例(相对安全,但仍需额外处理极端情况)。

  • 适用场景:绝大多数单例场景(推荐作为首选实现)。

5. 枚举(Enum):“天然防反射、防序列化”
  • 原理:Java 枚举的本质是特殊的类,JVM 保证枚举实例的唯一性,且天然防止反射和反序列化攻击。

  • 代码实现

    java

    运行

    public enum EnumSingleton {
        // 枚举实例(全局唯一)
        INSTANCE;
    
        // 枚举可以有自己的方法
        public void doSomething() {
            System.out.println("单例方法执行");
        }
    }
    
    // 使用:直接通过枚举名.INSTANCE调用
    public class Client {
        public static void main(String[] args) {
            EnumSingleton.INSTANCE.doSomething();
        }
    }
    
  • 优点

    • 绝对线程安全:枚举实例由 JVM 管理,加载时自动初始化,且不可被修改;
    • 防反射攻击:反射无法创建枚举实例(Constructor.newInstance() 会抛异常);
    • 防序列化攻击:枚举的反序列化会直接返回已有的实例,不会创建新对象。
  • 缺点:不支持懒加载(枚举类加载时就会初始化实例)。

  • 适用场景:对安全性要求极高的场景(如全局配置、加密工具)。

三、单例模式的 “破坏者” 与解决方案

即使实现了上述单例,仍可能被反射反序列化破坏(枚举除外),需要额外处理。

1. 反射攻击及防御
  • 攻击方式:通过反射调用私有构造器,强制创建新实例:

    java

    运行

    // 反射破坏单例(以静态内部类为例)
    Class<?> clazz = StaticInnerClassSingleton.class;
    Constructor<?> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true); // 绕过私有修饰符
    StaticInnerClassSingleton instance1 = (StaticInnerClassSingleton) constructor.newInstance();
    StaticInnerClassSingleton instance2 = StaticInnerClassSingleton.getInstance();
    System.out.println(instance1 == instance2); // false(单例被破坏)
    
  • 防御方案:在私有构造器中添加判断,若实例已存在则抛异常:

    java

    运行

    private StaticInnerClassSingleton() {
        if (SingletonHolder.INSTANCE != null) {
            throw new RuntimeException("禁止通过反射创建实例");
        }
    }
    
2. 反序列化攻击及防御
  • 攻击方式:将单例对象序列化后再反序列化,会生成新实例(枚举除外):

    java

    运行

    // 序列化破坏单例(需实现Serializable接口)
    StaticInnerClassSingleton instance1 = StaticInnerClassSingleton.getInstance();
    // 序列化
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
    oos.writeObject(instance1);
    // 反序列化
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
    StaticInnerClassSingleton instance2 = (StaticInnerClassSingleton) ois.readObject();
    System.out.println(instance1 == instance2); // false(单例被破坏)
    
  • 防御方案:重写 readResolve() 方法,反序列化时返回已有实例:

    java

    运行

    public class StaticInnerClassSingleton implements Serializable {
        // ... 其他代码省略 ...
    
        // 反序列化时调用,返回已有实例
        private Object readResolve() {
            return SingletonHolder.INSTANCE;
        }
    }
    

四、单例模式的应用场景

单例模式适用于 “全局只需要一个实例,且实例创建成本高或需要统一管理” 的场景:

  • 工具类:如日志工具(Logger)、日期工具(DateUtils),避免重复创建工具实例浪费资源。
  • 全局配置:如 Config 类,存储应用的全局配置(数据库连接参数、系统参数等),确保配置一致性。
  • 资源管理器:如数据库连接池(ConnectionPool)、线程池(ThreadPool),控制资源数量,避免资源耗尽。
  • 设备管理器:如打印机管理器(PrinterManager),同一台打印机只能被一个管理器控制。

五、单例模式的优缺点

优点缺点
1. 控制实例数量,节省系统资源;2. 提供全局访问点,方便统一管理;3. 避免资源多重占用(如文件锁冲突)。1. 单例类职责过重,违背 “单一职责原则”(既负责业务逻辑,又负责实例管理);2. 不利于测试(单例依赖全局状态,测试用例可能互相干扰);3. 多线程环境下需额外处理线程安全,实现复杂。

总结

单例模式的核心是 “唯一实例 + 全局访问”,不同实现方式各有优劣:

  • 简单场景用静态内部类(懒加载 + 线程安全 + 简洁);
  • 高并发场景用双重检查锁(兼顾安全与性能);
  • 安全性要求极高(防反射 / 反序列化)用枚举
  • 实例初始化成本低且必被使用用饿汉式

实际开发中需根据场景选择,同时注意防御反射和反序列化攻击,避免单例被破坏。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐