23种设计模式——单例模式(Singleton)详解
✅作者简介:大家好,我是 Meteors., 向往着更加简洁高效的代码写法与编程方式,持续分享Java技术内容。
🍎个人主页:Meteors.的博客
💞当前专栏: 设计模式
✨特色专栏: 知识分享
🥭本文内容: 23种设计模式——单例模式(Singleton)详解
📚 ** ps ** : 阅读文章如果有问题或者疑惑,欢迎在评论区提问或指出。
目录
一. 背景
二. 单例模式介绍
三. 单例模式使用场景
四. 单例模式实现方式
方式一:饿汉式(Eager Initialization)
方式二:懒汉式(Lazy Initialization)
方式三:枚举(Enum)
五. 各种实现方式对比
一. 背景
单例模式是项目中很常用的设计模式。在项目的配置管理器中经常使用,负责管理应用的全局配置信息。通过单例模式可以确保整个应用中只有一个配置管理器实例,统一管理所有配置。
二. 单例模式介绍
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
它的核心思想是:控制实例的数量,节约系统资源。
三. 单例模式使用场景
在许多场景下,我们只需要一个对象来完成全局性的工作,创建多个实例不仅没有必要,还会浪费资源,甚至导致程序行为异常。例如:
配置信息类:整个应用共享一份配置,读取和修改都通过同一个对象进行。
日志记录类:所有日志都通过一个日志器写入同一个文件,避免多实例操作文件导致内容错乱。
数据库连接池:管理数据库连接,需要全局唯一以便高效管理连接资源。
线程池:类似数据库连接池。
缓存系统:如 Redis 客户端,通常一个应用一个实例就够了。
工具类:一些只提供静态方法,没有自身状态的工具类,也常被设计为单例。
如果不使用单例模式,而是随意创建实例,可能会导致:
资源浪费:频繁创建和销毁对象开销大。
数据不一致:多个实例可能持有不同的状态(例如,配置被一个实例修改,另一个实例却不知道)。
程序错误:例如多个日志实例同时写一个文件,会导致日志内容混乱。
四. 单例模式实现方式
实现一个单例模式通常需要注意以下三点:
私有化构造方法:防止外部通过
new
关键字创建实例。内部创建并持有该私有静态实例:在类内部自己创建这个唯一的实例。
提供一个公共的静态方法:供外部获取这个唯一的实例。
根据实例创建的时机,主要分为两种模式:饿汉式和懒汉式。
方式一:饿汉式(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 类,从而初始化 INSTANCEpublic 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 开发,静态内部类是最佳选择,简单又安全。
如果需要防御高级的攻击(如反射、反序列化),或者实现一个表示状态的单例,枚举是最佳选择。
双重校验锁稍微复杂,但在一些特定场景(如需要延迟初始化且实例字段需要延迟初始化时)仍有其价值。
当然,单例模式有时也会限制之后相关类的异步实现,使用前要仔细考虑!
最后,
其它设计模式会陆续更新,希望文章对你有所帮助!