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