深入理解Java单例模式:确保类只有一个实例
文章目录
- 什么是单例模式?
- 为什么我们需要单例模式?
- 单例模式的常见实现方式
- 1. 饿汉式(Eager Initialization)
- 2. 懒汉式(Lazy Initialization)
- 3. 双重检查锁定(Double-Checked Locking - DCL)
- 4. 静态内部类(Static Inner Class)
- 5. 枚举(Enum)
- 单例模式的优缺点与选择建议
- 单例模式的典型应用场景
- So-
在软件开发中,我们经常会遇到这样的场景:某个类在整个应用程序生命周期中只需要一个实例。例如,配置管理器、日志记录器、线程池等。如果允许创建多个实例,可能会导致资源浪费、数据不一致或者行为异常。这时,**单例模式(Singleton Pattern)**就应运而生,它旨在确保一个类在任何情况下都只有一个实例,并提供一个全局访问点。
本文将深入探讨Java中单例模式的核心概念、各种实现方式、各自的优缺点,以及它在实际开发中的应用场景,并助您选择最适合的单例实现。
什么是单例模式?
单例模式是一种创建型设计模式,它的核心思想在于:
- 限制实例化: 确保一个类只拥有一个实例。
- 提供全局访问: 提供一个公共的访问点,让程序中的所有部分都能获取到这个唯一的实例。
想象一下你家里的遥控器,通常你只需要一个来控制电视,再多一个就会显得多余且可能造成混乱。单例模式就是为了解决这种“一个就够了”的需求。
为什么我们需要单例模式?
单例模式并非适用于所有场景,但它能有效地解决以下问题:
- 资源节约与控制: 对于像数据库连接池、线程池或大型配置对象这样的重量级资源,频繁创建和销毁实例会消耗大量内存和CPU。单例模式能确保这些资源只被创建一次,从而显著提高系统性能和资源利用率。
- 行为统一性: 当应用程序中需要一个全局唯一的协调者或控制器时(比如日志记录器),所有对该组件的操作都必须作用于同一个实例。单例模式保证了这一点,避免了数据不一致或行为偏差。
- 避免并发冲突: 在多线程环境中,如果多个实例同时修改共享数据,很容易引发竞态条件和数据错误。单例模式通过限制实例数量,从根本上减少了这类并发问题的可能性。
单例模式的常见实现方式
在Java中,实现单例模式有多种巧妙的方法,每种方法都有其独特的适用场景和考量。
1. 饿汉式(Eager Initialization)
“饿汉式”顾名思义,它像个急不可耐的“饿汉”,在类加载时就迫不及待地创建了实例,无论你是否立即需要它。
public class SingletonHungry {// 实例在类加载时就创建,并用 final 确保引用不可变private static final SingletonHungry instance = new SingletonHungry();// 私有构造函数,阻止外部通过 new 关键字创建实例private SingletonHungry() {}// 提供获取实例的全局访问点public static SingletonHungry getInstance() {return instance;}public void showMessage() {System.out.println("Hello from Hungry Singleton!");}
}
优点:
- 天生线程安全: 由于实例在类加载时即被创建,JVM 会保证其初始化过程的线程安全性,无需额外同步。
- 实现简单: 代码简洁直观,易于理解。
缺点:
- 非懒加载: 无论是否使用,实例都会被创建。如果实例创建过程耗时且不一定会被使用,这可能造成不必要的资源浪费。
2. 懒汉式(Lazy Initialization)
“懒汉式”则恰恰相反,它像个“懒汉”,直到第一次被需要时才创建实例。
a) 线程不安全版本
这是最基础的懒汉式实现,但它在多线程环境下是不安全的。
public class SingletonLazyUnsafe {private static SingletonLazyUnsafe instance; // 延迟加载,初始为 nullprivate SingletonLazyUnsafe() {}public static SingletonLazyUnsafe getInstance() {if (instance == null) { // 当多个线程同时满足此条件时,可能创建多个实例instance = new SingletonLazyUnsafe();}return instance;}
}
问题: 在高并发场景下,如果多个线程同时判断 instance == null
为真,它们可能同时进入 if
块,从而创建出多个单例实例,这违背了单例模式的初衷。因此,此版本在生产环境中绝不应使用。
b) 线程安全版本(通过 synchronized
关键字)
为了解决线程不安全问题,最直接的方法就是对 getInstance()
方法进行同步。
public class SingletonLazySafe {private static SingletonLazySafe instance;private SingletonLazySafe() {}public static synchronized SingletonLazySafe getInstance() { // 对整个方法加锁if (instance == null) {instance = new SingletonLazySafe();}return instance;}
}
优点:
- 懒加载: 实例只在第一次被调用时才创建,避免了不必要的资源占用。
- 线程安全:
synchronized
关键字保证了在任何时刻只有一个线程能进入该方法,从而确保实例的唯一性。
缺点:
- 性能开销: 每次调用
getInstance()
方法都会进行同步(加锁和释放锁),即使实例已经创建,这种频繁的同步操作也会带来不必要的性能损耗,尤其是在高并发场景下。
3. 双重检查锁定(Double-Checked Locking - DCL)
DCL 是对懒汉式的一种性能优化,它试图在保证线程安全的同时,减少同步的开销。
public class SingletonDCL {// 使用 volatile 关键字保证可见性和禁止指令重排序private static volatile SingletonDCL instance;private SingletonDCL() {}public static SingletonDCL getInstance() {if (instance == null) { // 第一次检查:无需加锁,性能高synchronized (SingletonDCL.class) { // 加锁if (instance == null) { // 第二次检查:确保在多线程环境下只有一个实例被创建instance = new SingletonDCL();}}}return instance;}
}
volatile
关键字为何如此重要?
在 instance = new SingletonDCL();
这行代码背后,JVM 会执行以下三步操作:
- 分配内存空间。
- 初始化对象。
- 将
instance
引用指向分配的内存地址。
如果没有 volatile
,JVM 可能会对步骤 2 和 3 进行指令重排序。这意味着在某个线程执行到步骤 3 时,instance
已经指向了内存地址,但对象可能尚未完全初始化。此时,另一个线程如果也调用 getInstance()
,它会发现 instance
不为 null
,直接返回这个未完全初始化的对象,从而引发错误。
volatile
关键字的作用在于:
- 保证可见性: 确保当一个线程修改了
instance
的值,其他线程能立即看到最新值。 - 禁止指令重排序: 强制保证
instance = new SingletonDCL();
的三步操作不会被重排序,从而避免了上述问题。
优点:
- 懒加载: 实例按需创建。
- 线程安全: 通过双重检查和
volatile
关键字保证了多线程环境下的正确性。 - 性能优化: 相较于全程同步,DCL 在实例创建后不再进行同步,显著提升了性能。
缺点:
- 实现复杂: 代码略显复杂,理解和正确使用
volatile
是关键。 - JDK 1.5 之前存在问题: 在某些早期 JVM 版本中,DCL 即使有
volatile
也可能存在问题(已被修复),但在 JDK 1.5 及以上版本是可靠的。
4. 静态内部类(Static Inner Class)
静态内部类是实现单例模式的优雅且高效的方式之一,它巧妙地结合了懒加载和线程安全,同时代码简洁。
public class SingletonStaticInnerClass {// 私有构造函数,防止外部直接创建private SingletonStaticInnerClass() {}// 静态内部类,只有在第一次调用 getInstance() 时才会被加载private static class SingletonHolder {// 实例在 SingletonHolder 类加载时创建,并用 final 确保引用不可变private static final SingletonStaticInnerClass INSTANCE = new SingletonStaticInnerClass();}// 提供获取实例的全局访问点public static SingletonStaticInnerClass getInstance() {return SingletonHolder.INSTANCE;}public void showMessage() {System.out.println("Hello from Static Inner Class Singleton!");}
}
原理:
- 当
SingletonStaticInnerClass
类被加载时,其静态内部类SingletonHolder
不会立即加载。 - 只有在第一次调用
getInstance()
方法时,JVM 才会去加载SingletonHolder
类。 SingletonHolder
类在加载时,其静态成员INSTANCE
会被初始化。JVM 保证类加载过程的线程安全性,因此INSTANCE
的创建是线程安全的。- 由于静态内部类只会被加载一次,所以
INSTANCE
也只会被创建一次。
优点:
- 懒加载: 实例只有在被首次请求时才创建。
- 天生线程安全: 利用了 JVM 类加载机制的线程安全特性,无需额外同步,性能高效。
- 实现优雅: 代码简洁明了,易于理解和维护。
缺点:
- 几乎没有明显缺点,是 强烈推荐 的实现方式。
5. 枚举(Enum)
枚举是实现单例模式最简洁、最安全、最推荐的方式。它不仅能保证单例的唯一性,还能天然地防止反射攻击和序列化问题。
public enum SingletonEnum {INSTANCE; // 唯一的单例实例,它本身就是 final 的public void showMessage() {System.out.println("Hello from Enum Singleton!");}
}
原理:
- Java 枚举类型的实例在类加载时就会被创建。
- JVM 会确保每个枚举常量都是单例的。
- 线程安全: 枚举的创建过程是线程安全的。
- 防反射攻击: Java 的反射机制无法通过
AccessibleObject.setAccessible(true)
来创建枚举实例,因为Enum
类的构造器本身就做了限制。 - 防序列化问题: 枚举类型在序列化和反序列化时,其机制会确保只返回唯一的枚举实例,而不会创建新的实例。
优点:
- 最简洁: 代码量最少,易于编写和阅读。
- 天生线程安全: 由 JVM 保证,无需担心并发问题。
- 防止反射攻击: 提供最强的单例保障。
- 防止序列化/反序列化问题: 避免了传统单例模式可能存在的序列化漏洞。
缺点:
- 非懒加载: 枚举实例在类加载时即被创建。
- 扩展性受限: 对于一些需要复杂初始化逻辑或继承关系的场景,枚举可能不如其他方式灵活。
单例模式的优缺点与选择建议
实现方式 | 懒加载 | 线程安全 | 优点 | 缺点 | 推荐指数 |
---|---|---|---|---|---|
饿汉式 | 否 | 是 | 实现简单,天生线程安全。 | 非懒加载,可能造成资源浪费。 | ★★★ |
懒汉式(不安全) | 是 | 否 | 懒加载。 | 线程不安全,绝不应用于生产。 | ☆ |
懒汉式(同步) | 是 | 是 | 懒加载,线程安全。 | 性能开销大,每次调用都需要同步。 | ★★ |
双重检查锁定 | 是 | 是 | 懒加载,线程安全,性能优化。 | 实现相对复杂,需要 volatile 关键字来避免指令重排序问题。 | ★★★★ |
静态内部类 | 是 | 是 | 懒加载,天生线程安全(JVM 保证),代码优雅。 | 无明显缺点。 | ★★★★★ |
枚举 | 否 | 是 | 最简洁、最安全(防反射、防序列化),天生线程安全。 | 非懒加载,对于需要复杂初始化逻辑的场景可能不够灵活。 | ★★★★★ |
单例模式的整体优缺点:
优点:
- 节约系统资源: 避免了不必要的对象创建,降低内存和GC压力。
- 控制实例数量: 严格限制了某个类只能有一个实例,确保了行为一致性。
- 提供全局访问点: 方便程序中任何地方获取并使用这个唯一的实例。
缺点:
- 扩展性差: 单例模式通常没有接口,修改其逻辑可能需要修改源代码,不利于扩展和测试。
- 对测试不友好: 单例的全局性可能导致测试间的耦合,难以进行独立的单元测试。
- 可能违反单一职责原则: 单例类除了本身的业务逻辑外,还承担了实例创建和管理的职责。
单例模式的典型应用场景
- 配置管理器: 读取应用程序的配置信息,确保所有模块都使用同一份配置。
- 日志记录器: 所有日志输出都通过同一个日志实例写入,保证日志文件的统一性。
- 数据库连接池: 管理和复用数据库连接,避免频繁创建和关闭连接。
- 线程池: 管理和调度线程,提高任务处理效率。
- 缓存: 全局缓存实例,用于存储常用数据,加快数据访问速度。
- ID 生成器: 某些需要生成唯一序列号或ID的服务。
So-
单例模式是一个强大而常用的设计模式,但选择正确的实现方式至关重要。
- 最推荐的选择: 在大多数情况下,静态内部类和枚举是实现单例的最佳实践。它们都能优雅地解决线程安全问题,同时具有不同的优缺点。
- 如果您追求极致的简洁、安全(防反射、防序列化),且不介意非懒加载,那么枚举是首选。
- 如果您需要懒加载,并且希望代码结构清晰,那么静态内部类是非常好的选择。
- DCL (双重检查锁定) 在 JDK 1.5+ 环境下也是一个可靠且性能优化的方案,但相对于静态内部类和枚举,其代码复杂性略高。
- 饿汉式适用于对懒加载没有要求,且实例创建成本较低的场景。
- 避免使用线程不安全的懒汉式。
- 理解单例模式的优缺点,并根据具体需求权衡利弊。 虽然它很方便,但过度使用单例可能导致系统高耦合,不利于扩展和测试。
相关文章:
深入理解Java单例模式:确保类只有一个实例
文章目录 什么是单例模式?为什么我们需要单例模式?单例模式的常见实现方式1. 饿汉式(Eager Initialization)2. 懒汉式(Lazy Initialization)3. 双重检查锁定(Double-Checked Locking - DCL&…...

JavaScript性能优化实战:从核心原理到工程实践的全流程解析
下面我给出一个较为系统和深入的解析,帮助你理解和实践“JavaScript 性能优化实战:从核心原理到工程实践的全流程解析”。下面的内容不仅解释了底层原理,也结合实际工程中的最佳模式和工具,帮助你在项目中贯彻性能优化理念&#x…...

【应用】Ghost Dance:利用惯性动捕构建虚拟舞伴
Ghost Dance是葡萄牙大学的一个研究项目,研究方向是探索人与人之间的联系,以及如何通过虚拟舞伴重现这种联系。项目负责人Cecilia和Rui利用惯性动捕创造出具有流畅动作的虚拟舞伴,让现实中的舞者也能与之共舞。 挑战:Ghost Danc…...

使用 Mechanical 脚本获取联合反作用力和力矩
介绍 在上一篇文章中,我们详细介绍了在 Ansys Mechanical 静态/瞬态结构、随机振动和/或响应谱分析中提取所有螺栓连接的反作用力的过程。他,我们将讨论如何使用 Python 代码结果对象对关节连接执行相同的作,这对于随机振动/响应谱分析非常有…...
Java垃圾回收机制详解:从原理到实践
Java垃圾回收机制详解:从原理到实践 前言 垃圾回收(Garbage Collection,简称GC)是Java虚拟机自动管理内存的核心机制之一。它负责自动识别和回收不再被程序使用的内存空间,从而避免内存泄漏和溢出问题。深入理解垃圾…...
thinkphp8.1 调用巨量广告API接口,刷新token
1、在mysql中建立表sys_token; CREATE TABLE sys_token (id int UNSIGNED NOT NULL,access_token varchar(50) COLLATE utf8mb4_general_ci NOT NULL,expires_in datetime NOT NULL,refresh_token varchar(50) COLLATE utf8mb4_general_ci NOT NULL,refresh_token_expires_in …...
物联网数据归档方案选择分析
最近在做数据统计分析。我在做数据分析时候,需要设计归档表。有两种方式, 方式1:年月日。 其中,日表是每小时数据,每台设备有24条数据 月表是每天数据,每台设备根据实际月天数插入 年表是每月数据,每台设备有12条数据。 方式2:年月日时。 其中,小时表,是每个设备每小…...

微服务架构下的服务注册与发现:Eureka 深度解析
📦 一、引言 🌐 微服务架构中服务注册与发现的核心价值 在微服务架构中,服务注册与发现是支撑系统可扩展性、高可用性和动态管理的关键基础。 ✅ 核心价值解析 动态扩展与弹性伸缩 服务实例可随时上线/下线,无需手动更新配置&am…...

Qt/C++学习系列之QButtonGroup的简单使用
Qt/C学习系列之QButtonGroup的简单使用 前言QButtonGroup刨析源码 具体使用界面设计具体函数使用初始化信号与槽函数(两种方式) 总结 前言 在练手项目中,使用了QButtonGroup。项目需求有互斥的要求,在使用QRadioButton的基础上&a…...

CETOL 6σ v12.1 三维公差分析软件现已可供下载
一、新版本发布 德克萨斯州麦金尼 — 2025年6月5日 —Sigmetrix 宣布其最新版本的 CETOL 6σ 公差分析软件(v12.1)现已可供立即下载。公差分析在诸多方面为企业发展带来益处。它通过平衡质量与制造成本,助力企业提升盈利能力。企业还可借此缩…...

【JavaEE】Spring Boot项目创建
Spring Boot介绍 在学习Spring Boot之前,我们先来认识一下Spring Spring官方是这样介绍的: 可以看到,Spring让Java程序更加快速,简单和安全。Spring对于速度,简单性和生产力的关注使其成为世界上最流行的Java框架 Sp…...

KAG与RAG在医疗人工智能系统中的多维对比分析
1、引言 随着人工智能技术的迅猛发展,大型语言模型(LLM)凭借其卓越的生成能力在医疗健康领域展现出巨大潜力。然而,这些模型在面对专业性、时效性和准确性要求极高的医疗场景时,往往面临知识更新受限、事实准确性不足以及幻觉问题等挑战。为解决这些问题,检索增强生成(…...
车牌识别技术解决方案
在城市化进程不断加速的背景下,小区及商业区域的车辆管理问题日益凸显。为解决这一问题,车牌识别技术应运而生,成为提升车辆管理效率与安全性的关键手段。本方案旨在详细介绍车牌识别系统的基本原理、功能设计、实施流程以及预期效益…...
C/C++ 面试复习笔记(4)
1.在多线程的 Linux 程序中,调用系统函数(如pthread_create 创建线程、pthread_mutex_lock 锁定互斥锁等)可能会返回错误码。 与单线程环境相比,多线程环境下的错误处理有哪些需要特别注意的地方?请举例说明如何在多线…...
Unity 大型手游碰撞性能优化指南
Unity 大型手游碰撞性能优化指南 版本: 2.1 作者: Unity性能优化团队 语言: 中文 前言 在Unity大型手游的开发征途中,碰撞检测如同一位隐形的舞者,它在游戏的物理世界中赋予物体交互的灵魂。然而,当这位舞者的舞步变得繁复冗余时,便会悄然消耗宝贵的计算资源,导致帧率下…...
Git仓库的创建
Git服务器准备 假设Git所在服务器为Ubuntu系统,IP地址10.17.1.5。 一. 准备运行git服务的git用户,这里用户名就直接设定为git。 1. 创建一个git用户组,并创建git用户。 sudo groupadd git sudo useradd git -g git 2. 创建git用户目录&…...

从零到一:Maven 快速入门教程
目录 Maven 简介Maven 是什么为什么使用 Maven? 安装 Maven下载 Maven 配置 Maven解压文件配置本地仓库保存路径配置国内仓库地址 Maven 的核心概念了解 pom.xml 文件坐标依赖范围生命周期compileprovidedruntimetestsystemimport 依赖传递依赖排除依赖循环 继承1. …...
DDD架构实战 领域层 事件驱动
目录 核心实现: 这种实现方式的优势: 在实际项目中,你可能需要: 事件驱动往往是在一个微服务内部实现的 领域时间是DDD架构中比较常见的概念 在领域层内部的一个模型更改了状态或者发生了一些行为 向外发送一些通知 这些通…...
c# List<string>.Add(s) 报错:UnsupportedOperationException
在使用c#读取目录下指定格式文件目录后,使用List<string>.Add 来保存文件名时,出现UnsupportedOperationException错误,找了好久不知道问题出在哪里。 以下是错误代码: using (var fbd new FolderBrowserDialog{Descripti…...

postman基础
前言 本次 Chat 将结合业界广为推崇和使用的 RestAPI 设计典范 Github API,详细介绍 Postman 接口测试工具的使用方法和实战技巧。 在开始这个教程之前,先聊一下为什么接口测试在现软件行业如此重要? 为什么我们要学习 Postman?…...

python训练营day45
知识点回顾: tensorboard的发展历史和原理tensorboard的常见操作tensorboard在cifar上的实战:MLP和CNN模型 效果展示如下,很适合拿去组会汇报撑页数: 作业:对resnet18在cifar10上采用微调策略下,用tensorbo…...
B+树知识点总结
核心目标:减少磁盘 I/O 数据库系统(如 MySQL)的主要性能瓶颈通常在于磁盘 I/O(读取和写入数据到物理硬盘的速度远慢于内存访问)。B 树的设计核心就是最大限度地减少访问数据时所需的磁盘 I/O 次数。 一、B 树的基本结…...

Halcon透视矩阵
在 Halcon中,透视变换矩阵用于将图像从一个视角转换到另一个视角,常用于图像校正和几何变换。以下是计算透视变换矩阵的步骤及代码示例。 透视形变图像校正的步骤 对图像左简单的处理,分割要校正的区域;提取区域的顶点坐标信息&…...
SpringCloud——OpenFeign
概述: OpenFeign是基于Spring的声明式调用的HTTP客户端,大大简化了编写Web服务客户端的过程,用于快速构建http请求调用其他服务模块。同时也是spring cloud默认选择的服务通信工具。 使用方法: RestTemplate手动构建: // 带查询…...

007-nlohmann/json 项目应用-C++开源库108杰
本课为 fswatch(第一“杰”)的示例项目加上对配置文件读取的支持,同时借助 第三“杰” CLI11 的支持,完美实现命令行参数与配置文件的逻辑统一。 012-nlohmann/json-4-项目应用 项目基于原有的 CMake 项目 HelloFSWatch 修改。 C…...

移动端测试岗位高频面试题及解析
文章目录 一、基础概念二、自动化测试三、性能测试四、专项测试五、安全与稳定性六、高级场景七、实战难题八、其他面题 一、基础概念 移动端测试与Web测试的核心区别? 解析:网络波动(弱网测试)、设备碎片化(机型适配&…...
gvim比较两个文件不同并合并差异
使用 gvim 比较两个文件的不同: 方式一,使用 gvim 同时打开两个待比较的文件。 比较通用方式是采用 gvim -d 选项,具体命令,如下: gvim -d <file1> <file2>方式二,先用 gvim 打开一个文件&am…...
App使用webview套壳引入h5(二)—— app内访问h5,顶部被手机顶部菜单遮挡问题,保留顶部安全距离
引入webview的页面添加safeAreaInsets,对weview的webviewStyles做处理 在myApp中改造 entry.vue代码如下 template><view class"entry-page" :style"{ paddingTop: safeAreaInsets.top px }"><web-view :webview-styles"we…...

Git GitHub Gitee
一、Git 是一个免费、开源的分布式版本控制系统。 版本控制:一种记录文件内容变化,以便将来查阅特定版本修订情况的系统。它最重要的就是可以记录文件修改历史记录,从而让用户可以看历史版本,方便版本切换。 1.和集中式版本控制…...
《深度体验 Egg.js:打造企业级 Node.js 应用的全景指南》
🚀 核心亮点:Koa 的二次觉醒 企业级基因:阿里多年双十一验证的框架稳定性插件化架构:config.plugins 实现功能模块即插即用渐进式演进:从 50 行代码到 5 万行代码的无缝扩容能力 🔧 实战配置解析ÿ…...