Java 单例模式:深度解析与应用

在软件开发领域,设计模式是解决常见设计问题的有效方案,而单例模式作为创建型设计模式中的一员,其重要性不容小觑。它能够确保一个类仅有一个实例,并提供全局访问点,这一特性在资源管理、配置信息读取、线程池管理以及日志记录等多个方面都发挥着关键作用。本文将深入探讨 Java 单例模式的多种实现方式、线程安全性、懒汉式与饿汉式的区别以及其应用场景等内容,助力读者全面且深入地理解并熟练运用这一设计模式。

一、单例模式的概念

单例模式的核心是对一个类的实例化次数加以限制,确保在整个应用程序运行期间,该类仅有一个实例存在。这个实例的创建时机有两种常见情况:一种是在类加载时就完成创建(饿汉式);另一种是在首次被访问时才进行创建(懒汉式)。并且,会提供一个公共的静态方法作为获取该实例的唯一途径。通过这种方式,单例模式能够有效地管控资源的使用,避免因重复创建实例而导致的资源浪费,同时也为全局资源的统一管理和访问提供了极大的便利。

二、单例模式的实现方式

(一)饿汉式单例

饿汉式单例在类加载阶段就创建实例。其优势在于线程安全性由 JVM 保障,因为类加载过程本身就是线程安全的。以下是一个典型的饿汉式单例实现示例:

public class EagerSingleton {// 私有静态实例,在类加载时就初始化private static final EagerSingleton instance = new EagerSingleton();// 私有构造函数,防止外部实例化private EagerSingleton() {}// 公共静态方法获取单例实例public static EagerSingleton getInstance() {return instance;}
}

在上述代码中,EagerSingleton 类的构造函数被私有化,从而有效阻止了外部类对其进行实例化操作。instance 变量在类加载时便被创建并完成初始化,由于其被 private static final 修饰,这就确保了在整个应用程序的生命周期内,该实例的唯一性。getInstance 方法则作为全局访问点,任何需要使用这个单例实例的地方,都可以通过调用此方法获取。

(二)懒汉式单例

懒汉式单例的特点是在首次被访问时才创建实例,这种方式在一定程度上能够节省资源,但需要特别关注线程安全问题。以下是一个简单的懒汉式单例示例:

public class LazySingleton {// 私有静态实例,初始化为 nullprivate static LazySingleton instance;// 私有构造函数private LazySingleton() {}// 公共静态方法获取单例实例,需同步以保证线程安全public static synchronized LazySingleton getInstance() {if (instance == null) {instance = new LazySingleton();}return instance;}
}

在这个示例中,instance 变量初始被赋值为 null。在 getInstance 方法中,首先会检查 instance 是否为 null,若为 null,则创建一个新的 LazySingleton 实例并赋值给 instance。这里通过使用 synchronized 关键字来确保线程安全,其作用是在多线程环境下,当一个线程进入 getInstance 方法并创建实例时,其他线程会被阻塞在同步块之外,直至第一个线程完成实例创建并释放锁。然而,这种方式在高并发场景下,性能可能会受到较大影响,因为每次获取实例都需要进行同步检查。

(三)双重检查锁定(DCL)单例

为了优化懒汉式单例在多线程环境下的性能表现,可以采用双重检查锁定机制。这种方式在保障线程安全的同时,能够有效减少不必要的同步开销。

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;}
}

在上述代码中,instance 变量使用 volatile 关键字进行修饰。这是由于在多线程环境下,指令重排可能会导致其他线程获取到尚未完全初始化的 instancevolatile 关键字能够确保变量的可见性,即一个线程对 instance 的修改能够立即被其他线程察觉,同时禁止指令重排,从而保证对象的初始化顺序正确无误。双重检查锁定机制首先进行一次非同步的检查,如果实例已经存在,那么直接返回,避免了不必要的同步操作;若实例不存在,则进入同步块再次检查并创建实例,以此确保线程安全。

(四)静态内部类单例

静态内部类单例是一种较为优雅的实现方式,它巧妙地融合了饿汉式和懒汉式的优点。

public class StaticInnerClassSingleton {// 私有构造函数private StaticInnerClassSingleton() {}// 静态内部类,在类加载时不会立即加载private static class SingletonHolder {// 静态实例,在静态内部类加载时创建private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();}// 公共静态方法获取单例实例public static StaticInnerClassSingleton getInstance() {return SingletonHolder.instance;}
}

在这个实现中,SingletonHolder 是一个静态内部类。instance 实例在 SingletonHolder 类加载时创建,由于静态内部类只有在被使用时才会加载,所以实现了懒加载的效果。同时,类加载过程的线程安全性确保了实例的唯一性,无需额外的同步机制,既保障了线程安全又提升了性能。

(五)枚举单例

使用枚举来实现单例模式是一种简洁且线程安全的绝佳方式。在 Java 中,枚举类型的实例天然具有单例特性,并且由 JVM 保证其唯一性和线程安全性。

public enum EnumSingleton {INSTANCE;// 可以在这里定义单例的其他方法和属性public void doSomething() {System.out.println("Doing something in EnumSingleton.");}
}

在上述代码中,EnumSingleton 是一个枚举类型,仅有一个实例 INSTANCE。可以在枚举中定义其他方法和属性,通过 EnumSingleton.INSTANCE 即可访问这个单例实例并调用其方法。

三、线程安全性分析

(一)饿汉式

饿汉式单例在类加载时就创建实例,由于类加载过程由 JVM 保证是线程安全的,所以在多线程环境下,无论多个线程同时访问 getInstance 方法多少次,获取到的都将是同一个预先创建好的实例,不会出现多个实例的情况。

(二)懒汉式

如前文所述,简单的懒汉式单例通过在 getInstance 方法上使用 synchronized 关键字来确保线程安全。在多线程环境中,当一个线程进入 getInstance 方法并创建实例时,其他线程会被阻塞在同步块之外,直至第一个线程完成实例创建并释放锁。这种方式虽然能够保证线程安全,但同步开销较大,尤其是在高并发场景下,会对性能产生明显的影响。

(三)双重检查锁定(DCL)

双重检查锁定机制借助 volatile 关键字和两次 if 检查来保障线程安全。第一次非同步检查能够减少不必要的同步开销,第二次同步块内的检查则确保了在多线程竞争的情况下,只有一个线程能够成功创建实例。volatile 关键字保证了变量的可见性和禁止指令重排,有效避免了其他线程获取到未完全初始化的实例。

(四)静态内部类

静态内部类单例利用了类加载的线程安全性。SingletonHolder 类只有在 getInstance 方法被调用时才会加载,而类加载过程是线程安全的,所以在多线程环境下不会出现多个实例的情况。

(五)枚举

枚举单例由 JVM 保证其线程安全性,在多线程环境下,无论多少个线程访问 EnumSingleton.INSTANCE,获取到的都将是同一个实例,并且不会出现实例化多次的问题。

四、懒汉式与饿汉式的区别

(一)创建时机

  • 饿汉式:在类加载时就创建实例,无论该实例是否在后续的程序运行中被实际使用,都会提前占用内存资源。这种方式适用于实例创建过程相对简单、占用资源较少,并且在应用程序启动后就需要立即使用单例实例的场景。例如,一些基础的配置类,其在应用启动时就需要被加载并使用。
  • 懒汉式:在首次被访问时才创建实例,延迟了实例的创建过程,只有在真正需要使用该实例时才会占用内存资源。对于那些创建过程复杂、资源消耗大或者不一定会被使用到的单例对象,懒汉式单例能够显著提高资源利用率。比如,某些涉及到复杂数据库连接或网络初始化的单例对象,如果采用饿汉式可能会在应用启动时就进行不必要的资源消耗,而懒汉式则可以避免这种情况。

(二)线程安全性

  • 饿汉式:由于类加载过程的线程安全性,饿汉式单例天生就是线程安全的,无需额外的同步机制。这使得在多线程环境下使用饿汉式单例时,无需担心线程安全问题,代码实现相对简单。
  • 懒汉式:简单的懒汉式单例需要借助同步机制(如 synchronized 关键字)来保证线程安全,这必然会带来一定的性能开销。尽管可以通过双重检查锁定等优化方式来减少同步开销,但代码相对复杂,并且需要考虑 volatile 关键字等因素,增加了代码的维护难度。

(三)性能表现

  • 饿汉式:在类加载时创建实例可能会导致应用程序的启动时间略微延长,尤其是当单例对象的创建过程较为复杂时。然而,在应用程序运行过程中,由于不需要进行同步检查,获取实例的速度较快,能够提供较好的运行时性能。
  • 懒汉式:在低并发场景下,懒汉式单例的性能表现可能较好,因为只有在需要时才创建实例,避免了不必要的资源占用。但在高并发场景下,如果同步机制处理不当,会导致性能大幅下降,因为多个线程可能会竞争锁资源,造成线程阻塞和等待,从而影响整体性能。

五、单例模式的应用场景

(一)资源共享与管理

例如数据库连接池,在应用程序中通常只需要一个数据库连接池实例来统一管理数据库连接资源。多个数据库操作可以共享这个连接池,通过单例模式能够方便地实现连接池的全局访问和资源管理,有效避免创建多个连接池导致的资源浪费和性能下降。如果每个数据库操作都创建自己的连接池,不仅会消耗大量的系统资源,还会增加数据库连接的管理复杂性,而单例模式的数据库连接池可以很好地解决这些问题。

(二)配置信息读取

应用程序的配置信息在整个运行期间通常是固定不变的,如数据库配置、日志配置等。可以采用单例模式创建一个配置管理器,负责读取和管理配置信息,其他模块则可以通过单例实例获取配置信息,这样能够确保配置信息的一致性和全局可用性。例如,在一个大型的企业级应用中,不同的模块可能都需要访问数据库配置信息,如果没有单例模式的配置管理器,每个模块都自行读取配置文件,可能会导致配置不一致的问题,并且增加了配置文件管理的难度。

(三)线程池管理

线程池在多线程编程中用于管理线程资源,提高线程的复用性和性能。使用单例模式创建线程池,可以在整个应用程序中共享同一个线程池实例,方便对线程任务进行统一的调度和管理,避免创建多个线程池带来的资源竞争和管理复杂性。例如,在一个 Web 应用中,多个请求处理可能都需要使用线程池来执行异步任务,如果每个请求都创建自己的线程池,会导致系统资源的过度消耗和线程管理的混乱,而单例模式的线程池可以有效地解决这些问题。

(四)日志记录器

在一个应用程序中,通常只需要一个日志记录器来记录各种日志信息。单例模式可以确保只有一个日志记录器实例存在,方便对日志的输出格式、级别、目标等进行统一管理和配置,并且在多线程环境下也能保证日志记录的顺序和完整性。例如,在一个分布式系统中,多个节点可能都会产生日志信息,如果每个节点都有自己的日志记录器,那么在日志收集和分析时会面临诸多困难,而单例模式的日志记录器可以将所有节点的日志信息统一管理,便于后续的处理和分析。

六、总结

Java 单例模式是一种极为实用的设计模式,通过限制类的实例化次数为一次,并提供全局访问点,在资源管理、配置信息处理、线程池和日志记录等众多场景中都有着广泛的应用。本文详细介绍了饿汉式、懒汉式、双重检查锁定、静态内部类和枚举等多种单例模式的实现方式,深入分析了它们的线程安全性、懒汉式与饿汉式的区别以及应用场景。在实际开发过程中,开发人员需要依据具体的需求和场景,仔细权衡资源占用、线程安全和性能等多方面因素,从而选择最为合适的单例模式实现方式,以此构建高效、可靠的 Java 应用程序。同时,随着 Java 语言的不断发展以及编程规范的持续演进,对于单例模式的理解和应用也需要不断深入和优化,以更好地适应日益复杂的软件开发需求。

希望通过本文的详细介绍,读者能够对 Java 单例模式有更为透彻的理解,并能够在实际项目开发中灵活自如地运用这一设计模式,从而有效提升软件设计和开发的质量与效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/61258.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

吉他初学者学习网站搭建系列(8)——如何练习音阶

文章目录 背景实现吉他面板音阶位置音阶识别 结语 背景 大家好,我是一个爱好音乐的非典型程序员!我最近又往自己的网站中集成了一个模块——音阶。下面介绍一下背景。 很多吉他初学者在掌握了一些音阶知识后,可能不知道怎么训练自己的对音阶…

15.三数之和 python

三数之和 题目题目描述示例 1:示例 2:示例 3:题目链接 题解Python 实现解释提交结果 题目 题目描述 给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k ,同时还满…

tauri使用github action打包编译多个平台arm架构和inter架构包踩坑记录

这些error的坑,肯定是很多人不想看到的,我的开源软件PakePlus是使用tauri开发的,PakePlus是一个界面化将任何网站打包为轻量级跨平台软件的程序,利用Tauri轻松构建轻量级多端桌面应用和多端手机应用,为了实现发布的时候…

Android 12.0 DocumentsUI文件管理器首次进入默认显示内部存储文件功能实现

1.前言 在12.0的系统rom定制化开发中,在关于文件管理器的某些功能中,在首次进入文件管理器的时候默认进入下载 文件夹,点击菜单选择内部存储的时候,会显示内部存储的内容,客户开发需要要求默认显示内部存储的文件 接下来分析下功能的实现 如图: 2.DocumentsUI文件管理器首…

抓包之wireshark基础用法介绍

写在前面 wireshark作为最优秀的抓包工具,有必要详细的看下其基本用法,所以本文就一起来做这件事吧! 1:初步介绍 打开wireshark首先会进入如下的界面: 想要开始抓包,需要进行如下操作: 接着…

【Java基础入门篇】二、控制语句和递归算法

Java基础入门篇 二、控制语句和递归算法 2.1 switch-case多分支选择语句 switch执行case语句块时,若没有遇到break,则运行下一个case直到遇到break,最后的default表示当没有case与之匹配时,默认执行的内容,代码示例如…

【人工智能学习之STGCN训练自己的数据集】

STGCN训练自己的数据集 准备事项数据集制作视频转jsonjsons转jsonjson转npy&pkl 训练STGCN添加图结构修改训练参数开始训练测试 准备事项 st-gcn代码下载与环境配置 git clone https://github.com/yysijie/st-gcn.git cd st-gcn pip install -r requirements.txt cd torc…

Dify+Docker

1. 获取代码 直接下载 (1)访问 langgenius/dify: Dify is an open-source LLM app development platform. Difys intuitive interface combines AI workflow, RAG pipeline, agent capabilities, model management, observability features and more, …

Android so库的编译

在没弄明白so库编译的关系前,直接看网上博主的博文,常常会觉得云里雾里的,为什么一会儿通过Android工程cmake编译,一会儿又通过NDK命令去编译。两者编译的so库有什么区别? android版第三方库编译总体思路: 对于新手小白来说搞明白上面的总体思路图很有必…

Java函数式编程+Lambda表达式

文章目录 函数式编程介绍纯函数Lambda表达式基础Lambda的引入传统方法1. 顶层类2. 内部类3. 匿名类 Lambda 函数式接口(Functional Interface)1. **函数式接口的定义**示例: 2. **函数式接口与Lambda表达式的关系**关联逻辑:示例&…

Linux操作系统2-进程控制3(进程替换,exec相关函数和系统调用)

上篇文章:Linux操作系统2-进程控制2(进程等待,waitpid系统调用,阻塞与非阻塞等待)-CSDN博客 本篇代码Gitee仓库:Linux操作系统-进程的程序替换学习 d0f7bb4 橘子真甜/linux学习 - Gitee.com 本篇重点:进程替换 目录 …

文件上传漏洞:你的网站安全吗?

文章目录 文件上传漏洞攻击方式:0x01绕过前端限制0x02黑名单绕过1.特殊解析后缀绕过2..htaccess解析绕过3.大小写绕过4.点绕过5.空格绕过6.::$DATA绕过7.配合中间件解析漏洞8.双后缀名绕过9.短标签绕过 0x03白名单绕过1.MIME绕过(Content-Type绕过)2.%00截断3.0x00截…

设计模式-适配器模式-注册器模式

设计模式-适配器模式-注册器模式 适配器模式 如果开发一个搜索中台,需要适配或接入不同的数据源,可能提供的方法参数和平台调用的方法参数不一致,可以使用适配器模式 适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至…

springboot341+vue校园求职招聘系统设计和实现pf(论文+源码)_kaic

毕 业 设 计(论 文) 校园求职招聘系统设计与实现 摘 要 传统办法管理信息首先需要花费的时间比较多,其次数据出错率比较高,而且对错误的数据进行更改也比较困难,最后,检索数据费事费力。因此,…

基于java web的网上书店系统设计

摘 要 随着互联网的越发普及,网上购物成为了当下流行的热门行为。网络上开店创业有许多的优势:投入少,启动 资金低,交易便捷。网上书店与传统的线下书店比起来优势巨大,网上书店的经营方式和销售渠道是不同与线下书 店…

Java设计模式——职责链模式:解锁高效灵活的请求处理之道

嘿,各位 Java 编程大神和爱好者们!今天咱们要一同深入探索一种超厉害的设计模式——职责链模式。它就像一条神奇的“处理链”,能让请求在多个对象之间有条不紊地传递,直到找到最合适的“处理者”。准备好跟我一起揭开它神秘的面纱…

Android 设备使用 Wireshark 工具进行网络抓包

背景 电脑和手机连接同一网络,想使用wireshark抓包工具抓取Android手机网络日志,有以下两种连接方法: Wi-Fi 网络抓包。USB 网络共享抓包。需要USB 数据线将手机连接到电脑,并在开发者模式中启用 USB 网络共享。 查看设备连接信…

redis大key和热key

redis中大key、热key 什么是大key大key可能产生的原因大key可能会造成什么影响如何检测大key如何优化删除大key时可能的问题删除大key的策略 热key热key可能导致的问题解决热key的方法 什么是大key 大key通常是指占用内存空间过大或包含大量元素的键值对。 数据量大&#xff…

SpringBoot源码-spring boot启动入口ruan方法主线分析(二)

12.刷新前操作 // 刷新前操作prepareContext(context, environment, listeners, applicationArguments, printedBanner);进入prepareContext private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,SpringApplicationRun…

使用 VLC 在本地搭建流媒体服务器 (详细版)

提示:详细流程 避坑指南 Hi~!欢迎来到碧波空间,平时喜欢用博客记录学习的点滴,欢迎大家前来指正,欢迎欢迎~~ ✨✨ 主页:碧波 📚 📚 专栏:音视频 目录 借助VLC media pl…