重学设计模式-单例模式

devtools/2025/1/24 5:01:49/

一、什么是单例模式

单例模式,从字面意思理解,就是保证一个类只有一个实例,并提供一个全局访问点来访问这个实例。想象一下,在一个大型游戏中,游戏的配置信息类,整个游戏运行期间只需要一份配置数据就够了,没必要创建多个相同的配置实例,这时候单例模式就派上用场了。

它的主要特点有三个:一是私有的构造函数,防止外部代码随意创建类的实例;二是指向唯一实例的私有静态变量;三是一个公有的静态方法,用于获取这个唯一实例。通过这三个要素,单例模式就能够牢牢把控住实例的唯一性。

为了更直观,咱们来看一段简单的 Java 代码示例:

 

public class Singleton {

// 私有静态变量,存放唯一实例

private static Singleton instance;

// 私有构造函数,阻止外部实例化

private Singleton() {}

// 公有静态方法,获取唯一实例

public static Singleton getInstance() {

if (instance == null) {

instance = new Singleton();

}

return instance;

}

}

在这段代码中,private static Singleton instance 就是那个保存唯一实例的 “秘密基地”,private Singleton() 构造函数上了一把 “锁”,不让外人随便闯入创建新对象,而 public static Singleton getInstance() 方法则像是一个 “门卫”,当有人需要这个实例时,它负责检查并提供。

[此处可以插入一张简单示意单例模式类结构的 UML 图,帮助读者视觉化理解,图大概展示类中有私有静态变量、私有构造函数、公有静态方法这几个关键元素,以及它们之间的关系]

二、单例模式的优点

  1. 节约系统资源

由于只存在一个实例,避免了重复创建对象带来的内存开销。比如说,在一个电商系统里,数据库连接池通常采用单例模式。创建数据库连接是比较耗费资源的操作,如果每次数据库操作都新建一个连接,系统资源很快就会被耗尽。而单例模式下,整个系统共享一个连接池实例,大大节省了资源,提高了系统的性能和稳定性。

  1. 全局唯一访问点

提供统一的访问入口,使得代码逻辑更加清晰。以操作系统中的任务管理器为例,无论在系统的哪个模块、哪个层级,当需要获取当前系统运行状态信息时,都可以通过任务管理器这个单例的全局访问点来获取,不用四处寻找不同的数据源,方便又可靠。

三、单例模式的实现方式

  1. 懒汉式(线程不安全)

咱们前面看到的示例代码其实就是懒汉式单例模式的一种简单实现。它的特点是在第一次调用 getInstance 方法时才去创建实例,也就是 “懒加载”,延迟了实例的创建时机。但这种方式在多线程环境下是不安全的。想象一下,多个线程同时进入 if (instance == null) 判断,都以为还没有实例,就会各自创建一个实例,这就违背了单例模式的初衷。

为了解决线程不安全问题,就有了下面的改进版。

  1. 懒汉式(线程安全)
 

public class ThreadSafeSingleton {

private static ThreadSafeSingleton instance;

private ThreadSafeSingleton() {}

public static synchronized ThreadSafeSingleton getInstance() {

if (instance == null) {

instance = new ThreadSafeSingleton();

}

return instance;

}

}

这里通过给 getInstance 方法加上 synchronized 关键字,使得在同一时刻只有一个线程能够进入这个方法,保证了多线程环境下的单例性。不过,这种方式的缺点是性能开销较大,因为每次调用 getInstance 方法都要获取锁,即使实例已经创建好了,也会有额外的开销。

  1. 饿汉式
 

public class EagerSingleton {

// 在类加载时就创建实例

private static final EagerSingleton instance = new EagerSingleton();

private EagerSingleton() {}

public static EagerSingleton getInstance() {

return instance;

}

}

与懒汉式不同,饿汉式在类加载阶段就创建好了实例,天生就是线程安全的,因为类加载过程由 JVM 保证是线程安全的。但它的缺点是可能会造成资源浪费,如果这个单例实例在程序运行很长一段时间后才会被用到,那么在前期就占用了内存空间。

  1. 双重检查锁(DCL)
 

public class DoubleCheckedLockingSingleton {

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 关键字很关键,它保证了变量的可见性,防止指令重排序导致的线程安全问题。在多线程高并发场景下,这种方式能在保证单例的同时,尽量减少性能损耗。

  1. 静态内部类
 

public class StaticInnerClassSingleton {

private StaticInnerClassSingleton() {}

private static class SingletonHolder {

private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();

}

public static StaticInnerClassSingleton getInstance() {

return SingletonHolder.instance;

}

}

这种方式利用了 Java 的静态内部类特性,当外部类被加载时,静态内部类不会立即加载,只有在调用 getInstance 方法时,静态内部类才会加载并创建实例,实现了延迟加载。同时,由于类加载机制保证了线程安全性,所以它既高效又线程安全,是一种比较推荐的实现方式。

[每介绍一种实现方式,都可以插入对应的简单示意代码执行流程的图片,比如用序列图展示多线程环境下不同实现方式中线程获取实例的过程,帮助读者更好理解代码运行逻辑]

四、单例模式的应用场景

  1. 日志记录器

在一个复杂的软件系统中,需要记录系统运行过程中的各种信息,如错误日志、操作日志等。使用单例模式的日志记录器可以保证整个系统的日志输出到同一个地方,方便管理和查看。不同模块只需调用日志记录器的单例实例,就能统一地记录日志,不会出现日志分散在各处,难以追踪的问题。

  1. 配置文件读取

软件通常需要读取配置文件来获取运行参数,像数据库连接字符串、服务器端口号等。配置文件读取类采用单例模式,确保整个系统使用的是同一套配置数据,避免因配置不一致导致的错误。而且,只需要在第一次使用时读取配置文件并缓存数据,后续直接从单例实例中获取,提高了效率。

  1. 线程池

线程池负责管理和调度线程,在多线程应用中,一个系统通常只需要一个线程池。通过单例模式创建线程池,能够统一分配线程资源,控制并发数量,防止线程创建过多导致系统崩溃,保障系统的稳定运行。

  1. 缓存管理

比如网页缓存,为了提高页面加载速度,会缓存已经访问过的页面内容。缓存管理器作为单例,可以全局控制缓存的存储、检索和清理,确保不同页面请求能高效共享缓存资源,减少重复的数据获取和处理。

五、单例模式的注意事项

  1. 生命周期管理

要注意单例对象的生命周期与应用程序的生命周期是否匹配。如果单例对象持有一些其他资源,在应用程序关闭时,需要确保这些资源被正确释放,否则可能导致资源泄露。

  1. 多线程并发

在多线程环境下,一定要选择合适的单例实现方式,避免出现线程安全问题。错误的实现可能会导致多个实例被创建,破坏单例模式的完整性,进而引发程序逻辑错误。

  1. 单元测试挑战

由于单例模式的特性,对依赖单例的代码进行单元测试时可能会遇到困难。比如,单例实例在全局只有一个,测试不同场景下的代码逻辑时,难以模拟不同的单例状态。这时候就需要一些特殊的测试技巧,如使用依赖注入框架,在测试时能够替换单例实例,方便进行单元测试。

总之,单例模式是编程中一个非常实用的设计模式,它在节约资源、提供统一访问等方面有着显著优势。但在使用过程中,要根据具体场景选择合适的实现方式,并注意相关的注意事项,才能让单例模式真正为我们的软件项目增光添彩。希望通过这篇博客,大家都能对单例模式有一个深入的了解,并能在自己的编程之旅中灵活运用。


http://www.ppmy.cn/devtools/153047.html

相关文章

如何运用python爬虫获取大型资讯类网站文章,并同时导出pdf或word格式文本?

这里,我们以比较知名的商业新知网站https://www.shangyexinzhi.com/为例进行代码编写,下面进行代码应用思路。 第一部分,分析网站结构 首先,我们来分析,要使用Python技术分析一个网站的结构,通常可以通过…

使用 JMeter 的 Autostop Listener 插件:自动化性能测试的守护者

在性能测试中,监控测试执行的状态并及时做出响应是至关重要的。如果测试过程中出现性能瓶颈或系统崩溃,继续运行测试可能会导致资源浪费或测试结果不准确。JMeter 的 Autostop Listener 插件正是为了解决这一问题而设计的。它允许你设置自动化停止条件&a…

【Ubuntu】安装SSH启用远程连接

【Ubuntu】安装OpenSSH启用远程连接 零、安装软件 使用如下代码安装OpenSSH服务端: sudo apt install openssh-server壹、启动服务 使用如下代码启动OpenSSH服务端: sudo systemctl start ssh贰、配置SSH(可跳过) 配置文件 …

两两交换链表中的节点

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 思路 这道题目正常模拟就行了。//我还不熟练如何模拟,我在这方面还是差了点,毕竟还算是新手。所有链表都建议使用虚拟头结点。 ListNode *swap(ListNode *head) {Lis…

【vitePress】基于github快速添加评论功能(giscus)

一.添加评论插件 使用giscus来做vitepress 的评论模块,使用也非常的简单,具体可以参考:giscus 文档,首先安装giscus npm i giscus/vue 二.giscus操作 打开giscus 文档,如下图所示,填入你的 github 用户…

C# 委托和事件(事件)

回调(callback)函数是Windows编程的一个重要部分。C或C编程背景,在许多Windows API中使用过回调。VB添加AddressOf关键字后,开发人员就可以利用以前一度受到限制的API。回调函数实际上是方法调用的指针也称为函数指针。.NET以委托的形式实现函…

SpringBoot为什么要禁止循环依赖?

大家好,我是锋哥。今天分享关于【SpringBoot为什么要禁止循环依赖?】面试题。希望对大家有帮助; SpringBoot为什么要禁止循环依赖? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring Boot 禁止循环依赖的原因与 Spring 框架本身的设计…

如何判断以太坊地址类型?

如何判断以太坊地址类型? 一、账户类型解释 2.1 以太坊外部账户(Externally Owned Account,EOA) 外部账户(EOA)是由私钥控制的账户,在以太坊网络中用来发送交易和执行其他操作。EOA 不是智能…