当前位置: 首页 > news >正文

单例模式介绍

1. 什么是单例模式?

单例模式是一种创建型设计模式,它的核心思想是保证一个类只有一个实例,并提供一个全局访问点来访问这个唯一的实例。

你可以把它想象成一个国家的国王或一个公司的CEO。在任何时候,都只能有一个人担任这个角色,所有人都通过统一的渠道(如朝廷、董事会)来向他/她汇报或请求指示。

主要目的:

  • 控制资源访问:确保某个资源(如数据库连接池、线程池、配置管理器)在整个应用程序中只有一个实例,避免资源浪费和冲突。
  • 保证数据一致性:当多个地方需要共享和操作同一份数据时,单例可以确保数据的一致性。
  • 方便管理:提供一个统一的入口来访问该实例,便于集中管理和控制。

2. 单例模式的 UML 类图

单例模式的结构非常简单,通常只包含一个角色:单例类

+---------------------+
|     Singleton       |
+---------------------+
| - instance: Singleton|  // 私有的静态实例
+---------------------+
| - Singleton()       |  // 私有的构造函数
| + getInstance():    |  // 公有的静态获取实例方法
|      Singleton      |
+---------------------+

关键要素:

  1. 私有静态成员变量 (instance):用于存储单例类的唯一实例。由于是静态的,它属于类本身,而不是类的某个对象。
  2. 私有构造函数 (Singleton()):这是实现单例的“防火墙”。将构造函数设为私有,可以防止外部代码通过 new Singleton() 的方式创建新的实例。
  3. 公有静态方法 (getInstance()):这是全局访问点。外部代码通过调用这个方法来获取单例实例。该方法负责控制实例的创建过程,确保只创建一次。

3. 单例模式的多种实现方式(Java 示例)

单例模式有多种实现方式,每种方式在线程安全性能实现复杂度上各有优劣。

方式一:饿汉式

思想:在类加载时就立即创建实例,就像一个“饿汉”一样,不管你需不需要,我先创建好再说。

public class EagerSingleton {// 1. 私有静态成员变量,在类加载时就初始化private static final EagerSingleton instance = new EagerSingleton();// 2. 私有构造函数,防止外部 newprivate EagerSingleton() {// 防止通过反射攻击if (instance != null) {throw new IllegalStateException("Singleton already initialized");}}// 3. 公有静态方法,直接返回已创建好的实例public static EagerSingleton getInstance() {return instance;}
}
  • 优点
    • 实现简单:代码最简洁。
    • 线程安全:由于实例是在类加载时创建的,由 JVM 保证了其线程安全性。static final 关键字确保了实例的唯一性和不可变性。
  • 缺点
    • 可能造成资源浪费:如果这个单例对象非常大,并且在整个应用程序运行期间都没有被使用,那么它就会一直占用内存,造成不必要的资源开销。
方式二:懒汉式(线程不安全)

思想:延迟加载,只有在第一次调用 getInstance() 方法时才创建实例。

public class LazySingleton {// 1. 私有静态成员变量,先不初始化private static LazySingleton instance;// 2. 私有构造函数private LazySingleton() {}// 3. 公有静态方法,获取实例时才创建public static LazySingleton getInstance() {if (instance == null) { // 第一次检查instance = new LazySingleton();}return instance;}
}
  • 优点
    • 延迟加载:避免了饿汉式可能造成的资源浪费,实现了按需创建。
  • 缺点
    • 线程不安全:在多线程环境下,如果多个线程同时通过 if (instance == null) 检查,并且都发现 instance 为 null,那么它们就会各自创建一个实例,从而破坏了单例模式。这种方式在生产环境中绝对不能使用!
方式三:懒汉式(同步方法,线程安全)

思想:为了解决方式二的线程安全问题,在 getInstance() 方法上加上 synchronized 关键字,使其成为同步方法。

public class SynchronizedLazySingleton {private static SynchronizedLazySingleton instance;private SynchronizedLazySingleton() {}// 使用 synchronized 关键字修饰方法public static synchronized SynchronizedLazySingleton getInstance() {if (instance == null) {instance = new SynchronizedLazySingleton();}return instance;}
}
  • 优点
    • 线程安全synchronized 确保了同一时间只有一个线程能进入该方法,保证了实例的唯一性。
    • 延迟加载:保留了懒汉式的优点。
  • 缺点
    • 性能低下:每次调用 getInstance() 方法都会进行同步,即使实例已经被创建。同步操作会带来性能开销,而这个锁只有在第一次创建实例时才是必要的。之后每次获取实例都需要等待锁,效率很低。
方式四:双重检查锁定 - 推荐

思想:这是对方式三的优化。我们只在第一次创建实例时进行同步,后续获取实例时无需同步,从而兼顾了线程安全和性能。

public class DoubleCheckedLockingSingleton {// 使用 volatile 关键字修饰,防止指令重排序private static volatile DoubleCheckedLockingSingleton instance;private DoubleCheckedLockingSingleton() {}public static DoubleCheckedLockingSingleton getInstance() {// 第一次检查(无锁),如果实例已存在,直接返回if (instance == null) {synchronized (DoubleCheckedLockingSingleton.class) {// 第二次检查(加锁),确保只有一个线程能创建实例if (instance == null) {instance = new DoubleCheckedLockingSingleton();}}}return instance;}
}
  • 为什么需要 volatile
    instance = new Singleton() 这行代码并非原子操作,它大致分为三步:

    1. memory = allocate(); // 分配对象的内存空间
    2. ctorInstance(memory); // 调用构造函数,初始化对象
    3. instance = memory; // 将 instance 指向分配的内存地址

    由于 JVM 的指令重排序优化,步骤 2 和 3 的顺序可能颠倒。如果线程 A 执行完 1 和 3,但还没执行 2,此时 instance 已经不为 null。线程 B 在第一次检查时发现 instance 不为 null,就会直接返回一个尚未初始化完成的实例,导致程序出错。volatile 关键字可以禁止指令重排序,确保 2 和 3 按顺序执行。

  • 优点

    • 线程安全:DCL 机制保证了多线程环境下的正确性。
    • 延迟加载:保留了懒加载的优点。
    • 性能高:只有在第一次创建实例时才需要同步,后续调用无需加锁,性能几乎与饿汉式相当。
  • 缺点

    • 实现稍复杂:需要理解 volatile 和 DCL 的原理。
方式五:静态内部类 - 强烈推荐

思想:利用 Java 的类加载机制来保证线程安全和延迟加载。这是目前公认的最佳实现方式之一。

public class StaticInnerClassSingleton {// 私有构造函数private StaticInnerClassSingleton() {}// 静态内部类private static class SingletonHolder {// 静态内部类中的静态成员变量private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();}// 公有静态方法,返回内部类的实例public static StaticInnerClassSingleton getInstance() {return SingletonHolder.INSTANCE;}
}
  • 工作原理

    1. 延迟加载StaticInnerClassSingleton 类被加载时,其静态内部类 SingletonHolder 并不会被立即加载。只有当第一次调用 getInstance() 方法时,SingletonHolder 类才会被加载。
    2. 线程安全:JVM 在加载类时,会保证其静态成员变量的初始化是线程安全的。因此,INSTANCE 的创建过程由 JVM 保证,不会有线程安全问题。
  • 优点

    • 线程安全:由 JVM 保证,无需任何同步代码。
    • 延迟加载:完美实现了按需加载。
    • 实现简单:代码比 DCL 更简洁,没有 volatile 和同步块,可读性高。
    • 性能高:没有同步开销。
方式六:枚举

思想:利用 Java 枚举类型的特性来实现单例。这是《Effective Java》作者 Josh Bloch 极力推荐的方式。

public enum EnumSingleton {// 这个枚举元素本身就是单例INSTANCE;// 可以添加你需要的任何方法public void doSomething() {System.out.println("Singleton is doing something.");}
}// 如何使用
public class Main {public static void main(String[] args) {EnumSingleton singleton = EnumSingleton.INSTANCE;singleton.doSomething();}
}
  • 优点
    • 绝对线程安全:枚举的实现同样由 JVM 保证。
    • 防止反射攻击:普通单例可以通过反射调用私有构造函数来创建新实例,但枚举不行,JVM 会阻止这种操作。
    • 防止序列化/反序列化破坏单例:普通单例在序列化再反序列化后,会创建一个新对象。而枚举在序列化时,JVM 只是保证了枚举实例的唯一性。
    • 代码极其简洁:是所有实现方式中最简单的。
  • 缺点
    • 不够灵活:由于枚举的特性,这种方式不太适用于需要继承父类或实现特定接口的场景(虽然可以实现接口,但感觉上有些奇怪)。对于大多数场景,这不是问题。

4. 单例模式的优缺点

优点
  1. 内存占用少:只有一个实例,避免了频繁创建和销毁对象带来的性能开销。
  2. 访问方便:提供了全局唯一的访问点,方便统一管理。
  3. 资源共享:适合需要共享资源的场景,如配置文件、日志工具、数据库连接池等。
缺点
  1. 扩展性差:由于构造函数私有,单例类通常不能被继承,这限制了它的扩展性。
  2. 职责过重:单例模式既承担了业务逻辑,又承担了对象创建管理的职责,在一定程度上违背了“单一职责原则”。
  3. 测试困难:在单元测试中,如果单例对象持有状态,那么测试之间可能会相互影响,因为单例是全局共享的。需要特别注意测试的隔离性。
  4. 滥用风险:因为使用方便,容易被滥用。在不适合的场景下使用单例,可能会导致程序结构混乱、耦合度增高。

5. 单例模式的适用场景

  • 需要频繁实例化然后销毁的对象:如线程池、缓存、日志对象等。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象:如数据库连接池。
  • 工具类对象:如配置文件管理器、JSON/Xml 解析工具等。
  • 需要频繁访问数据库或文件的对象:如数据访问层(DAO)的实例。
  • 系统中只需要一个对象来协调行为:如 Windows 的任务管理器、回收站。

6. 最佳实践与总结

实现方式线程安全延迟加载性能推荐度备注
饿汉式⭐⭐⭐⭐简单,但可能浪费资源
懒汉式(不安全)绝对禁止使用
懒汉式(同步方法)⭐⭐性能差,不推荐
双重检查锁定⭐⭐⭐⭐⭐强烈推荐,兼顾了所有优点
静态内部类⭐⭐⭐⭐⭐强烈推荐,代码更优雅
枚举⭐⭐⭐⭐⭐最佳实现,最安全,但不够灵活

如何选择?

  • 如果单例对象占用资源不大,且希望实现简单饿汉式是一个不错的选择。
  • 如果需要延迟加载,并且对代码简洁性要求高静态内部类是你的首选。它在绝大多数情况下都是完美的解决方案。
  • 如果需要延迟加载,并且单例类需要继承或实现复杂的逻辑双重检查锁定是最佳选择。
  • 如果对安全性要求极高,且不介意使用枚举枚举是理论上最完美的实现方式,能抵御反射和序列化攻击。

在现代 Java 开发中,静态内部类双重检查锁定是最常用和最受推崇的实现方式。选择哪种取决于你的具体需求和偏好。

http://www.dtcms.com/a/347665.html

相关文章:

  • 企业视频库管理高效策略
  • Java和数据库的关系
  • 如何利用 DeepSeek 提升工作效率
  • C++的struct里面可以放函数,讨论一下C++和C关于struct的使用区别
  • 基于TimeMixer现有脚本扩展的思路分析
  • 网络参考模型操作指南
  • 大数据接口 - 企业风险报告(专业版)API接口文档
  • 【Vue✨】Vue 中的 diff 算法详解
  • Compose笔记(四十七)--SnackbarHost
  • 14.Shell脚本修炼手册--玩转循环结构(While 与 Until 的应用技巧与案例)
  • 使用sys数据库分析 MySQL
  • 2015-2018年咸海流域1km归一化植被指数8天合成数据集
  • 【大模型应用开发 4.RAG高级技术与实践】
  • LeetCode算法日记 - Day 20: 两整数之和、只出现一次的数字II
  • 《P3623 [APIO2008] 免费道路》
  • Java22 stream 新特性 窗口算子 与 虚拟线程map操作:Gatherer 和 Gatherers工具类
  • 告别静态网页:我用Firefly AI + Spline,构建次世代交互式Web体验
  • 学习Java24天
  • React学习(十二)
  • IDEA相关的设置和技巧
  • C语言第十一章内存在数据中的存储
  • Redis资料
  • JAVA读取项目内的文件或图片
  • springboot项目结构
  • Axure:如何打开自定义操作界面
  • 顺序表(ArrayList)
  • 刷题日记0823
  • [特殊字符] 数据库知识点总结(SQL Server 方向)
  • MySQL:事务管理
  • games101 作业0 环境搭建与熟悉线性代数库