23种设计模式——单例模式(Singleton)详解

✅作者简介:大家好,我是 Meteors., 向往着更加简洁高效的代码写法与编程方式,持续分享Java技术内容。
🍎个人主页:Meteors.的博客
💞当前专栏: 设计模式

✨特色专栏: 知识分享
🥭本文内容: 23种设计模式------单例模式(Singleton)​详解
📚 ** ps ** : 阅读文章如果有问题或者疑惑,欢迎在评论区提问或指出。


目录

[一. 背景](#一. 背景)

[二. 单例模式介绍](#二. 单例模式介绍)

[三. 单例模式使用场景](#三. 单例模式使用场景)

[四. 单例模式实现方式](#四. 单例模式实现方式)

[方式一:饿汉式(Eager Initialization)](#方式一:饿汉式(Eager Initialization))

[方式二:懒汉式(Lazy Initialization)](#方式二:懒汉式(Lazy Initialization))

方式三:枚举(Enum)

[五. 各种实现方式对比](#五. 各种实现方式对比)


一. 背景

单例模式是项目中很常用的设计模式。在项目的配置管理器中经常使用,负责管理应用的全局配置信息。通过单例模式可以确保整个应用中只有一个配置管理器实例,统一管理所有配置。

二. 单例模式介绍

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

它的核心思想是:​​控制实例的数量,节约系统资源​​。


三. 单例模式使用场景

在许多场景下,我们只需要一个对象来完成全局性的工作,创建多个实例不仅没有必要,还会浪费资源,甚至导致程序行为异常。例如:

  • ​配置信息类​​:整个应用共享一份配置,读取和修改都通过同一个对象进行。

  • ​日志记录类​​:所有日志都通过一个日志器写入同一个文件,避免多实例操作文件导致内容错乱。

  • ​数据库连接池​​:管理数据库连接,需要全局唯一以便高效管理连接资源。

  • ​线程池​​:类似数据库连接池。

  • ​缓存系统​​:如 Redis 客户端,通常一个应用一个实例就够了。

  • ​工具类​​:一些只提供静态方法,没有自身状态的工具类,也常被设计为单例。

如果不使用单例模式,而是随意创建实例,可能会导致:

  • ​资源浪费​​:频繁创建和销毁对象开销大。

  • ​数据不一致​​:多个实例可能持有不同的状态(例如,配置被一个实例修改,另一个实例却不知道)。

  • ​程序错误​​:例如多个日志实例同时写一个文件,会导致日志内容混乱。


四. 单例模式实现方式

实现一个单例模式通常需要注意以下三点:

  1. ​私有化构造方法​ ​:防止外部通过 new关键字创建实例。

  2. ​内部创建并持有该私有静态实例​​:在类内部自己创建这个唯一的实例。

  3. ​提供一个公共的静态方法​​:供外部获取这个唯一的实例。

根据实例创建的时机,主要分为两种模式:​​饿汉式​ ​和​​懒汉式​​。

方式一:饿汉式(Eager Initialization)

类加载时就直接初始化实例。​​简单、线程安全,但可能造成资源浪费​​(如果实例一直没被用到)。

复制代码
public class EagerSingleton {
    // 1. 在类加载时就创建好实例
    private static final EagerSingleton INSTANCE = new EagerSingleton();

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

    // 3. 提供全局访问点
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

​优点​​:实现简单,线程安全(由 JVM 类加载机制保证)。

​缺点​​:如果实例很大且从未使用,会造成内存浪费。


方式二:懒汉式(Lazy Initialization)

延迟加载,只有在第一次被调用时才创建实例。

​a) 基础版本(线程不安全)​

多线程环境下可能创建多个实例。

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

    private UnsafeLazySingleton() {}

    public static UnsafeLazySingleton getInstance() {
        // 如果实例不存在,则创建
        if (instance == null) {
            instance = new UnsafeLazySingleton();
        }
        return instance;
    }
}

​b) 同步方法版(线程安全但效率低)​

通过 synchronized加锁保证线程安全,但每次获取实例都要同步,性能差。

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

    private SynchronizedLazySingleton() {}

    // 使用 synchronized 关键字修饰方法
    public static synchronized SynchronizedLazySingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedLazySingleton();
        }
        return instance;
    }
}

​c) 双重校验锁(DCL, Double-Checked Locking)​

​推荐写法​​。在加锁前后进行两次检查,兼顾线程安全和性能。

复制代码
public class DCLSingleton {
    // 使用 volatile 关键字禁止指令重排序,保证可见性
    private static volatile DCLSingleton instance;

    private DCLSingleton() {}

    public static DCLSingleton getInstance() {
        // 第一次检查,避免不必要的同步
        if (instance == null) {
            // 同步代码块
            synchronized (DCLSingleton.class) {
                // 第二次检查,确保实例在同步块内未被创建
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

volatile关键字在这里至关重要,它防止了 new DCLSingleton()这一步的指令重排序,避免了其他线程获取到一个未初始化完成的对象。

​d) 静态内部类(Holder Class)​

​最优雅、最推荐的实现方式之一​​。利用 JVM 的类加载机制保证线程安全,同时实现了懒加载。

复制代码
public class InnerClassSingleton {
    // 私有化构造方法
    private InnerClassSingleton() {}

    // 静态内部类持有实例
    private static class SingletonHolder {
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }

    // 调用 getInstance 时,才会加载 SingletonHolder 类,从而初始化 INSTANCE
    public static InnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

​优点​​:

  • ​懒加载​ ​:只有在调用 getInstance()时,内部类 SingletonHolder才会被加载,实例才会被创建。

  • ​线程安全​​:由 JVM 在类加载时完成初始化,天然线程安全。

  • ​实现简单​​:无需同步代码块,代码简洁。


方式三:枚举(Enum)

​《Effective Java》作者 Josh Bloch 强烈推荐的方式​​。它不仅能避免多线程同步问题,还能防止反序列化重新创建新的对象。

复制代码
public enum EnumSingleton {
    INSTANCE; // 唯一的实例

    // 可以添加任意方法
    public void doSomething() {
        System.out.println("Doing something by " + this.toString());
    }
}

// 使用方式
EnumSingleton.INSTANCE.doSomething();

​优点​​:

  • ​绝对防止多实例​​:由 JVM 从根本上保证。

  • ​防止反射攻击​​:枚举类不能通过反射创建实例。

  • ​防止反序列化​​:枚举类在反序列化时不会创建新对象。

  • ​代码极简​​。

​缺点​​:不够灵活(例如无法实现延迟初始化)。


五. 各种实现方式对比

实现方式 懒加载 线程安全 性能 防反射/反序列化 推荐度
​饿汉式​ ⭐⭐
​同步方法懒汉式​
​双重校验锁(DCL)​ ⭐⭐⭐⭐
​静态内部类​ ⭐⭐⭐⭐⭐
​枚举​ ⭐⭐⭐⭐⭐

​**​选择?:**​

  • 如果对内存不敏感,追求极致的简单,可以用​​饿汉式​​。

  • 如果需要懒加载,且是现代 Java 开发,​​静态内部类​​是最佳选择,简单又安全。

  • 如果需要防御高级的攻击(如反射、反序列化),或者实现一个表示状态的单例,​​枚举​​是最佳选择。

  • ​双重校验锁​​稍微复杂,但在一些特定场景(如需要延迟初始化且实例字段需要延迟初始化时)仍有其价值。

当然,单例模式有时也会限制之后相关类的异步实现,使用前要仔细考虑!


最后,

其它设计模式会陆续更新,希望文章对你有所帮助!

相关推荐
张小姐的猫19 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
杨充1 天前
10.可测试性实战设计
设计模式·开源·代码规范
杨充1 天前
9.重构十二式的实战
设计模式·开源·代码规范
杨充1 天前
6.设计原则的全景图
设计模式·开源·全栈
杨充1 天前
2.面向对象的特性
设计模式
杨充1 天前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充1 天前
8.反模式与坏味道
设计模式·开源·代码规范
杨充1 天前
3.接口vs抽象类比较
设计模式
咖啡八杯1 天前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法