【Java进阶】java设计模式之单例模式
一、单例设计模式的基本概念
在 Java 编程的广阔天地里,单例设计模式宛如一颗璀璨的明星,是一种极为实用的创建型设计模式。它的核心使命是确保一个类在整个应用程序的生命周期内仅仅存在一个实例,并且为外界提供一个全局唯一的访问点来获取这个珍贵的实例。
想象一下,在一个大型的软件系统中,数据库连接池就像是一座城市的供水系统,需要稳定且唯一的管理;日志记录器则如同城市的档案馆,所有的信息都应该汇聚到一处。如果这些关键资源被随意创建多个实例,就好比城市有多个独立的供水系统和档案馆,不仅会造成资源的极大浪费,还可能引发数据不一致等严重问题,导致整个系统陷入混乱。而单例模式就像是一位精明的城市规划师,严格把控着实例的创建,保证一切井然有序。
二、单例设计模式的多种实现方式
1. 饿汉式单例
原理
饿汉式单例就像是一个“急性子”,在类加载的时候就迫不及待地创建了单例实例,不管后续是否会真正使用到这个实例。这种方式巧妙地利用了 Java 的类加载机制,天然地避免了多线程环境下的同步问题。
示例代码
class A {// 在类加载时就创建单例实例private static final A INSTANCE = new A();// 私有构造函数,防止外部通过 new 关键字创建实例private A() {}// 提供一个公共的静态方法,用于获取单例实例public static A getInstance() {return INSTANCE;}
}
为什么使用 static final 修饰 INSTANCE
-
static关键字的作用:在 Java 中,static关键字用于修饰类的成员(变量或方法),使其属于类本身,而不是类的某个实例。当一个变量被声明为static时,它在内存中只有一份拷贝,被所有该类的实例共享。在单例模式中,我们希望INSTANCE是一个全局唯一的实例,使用static修饰可以确保无论创建多少个A类的实例(实际上单例模式不允许外部创建多个实例),INSTANCE始终只有一个。而且,由于static变量在类加载时就会被初始化,所以INSTANCE会在类加载阶段就被创建出来。 -
final关键字的作用:final关键字用于修饰变量时,表示该变量是一个常量,一旦被赋值就不能再被修改。在单例模式中,我们希望INSTANCE是一个不可变的引用,即它一旦指向了某个A类的实例,就不能再指向其他实例。使用final修饰INSTANCE可以保证这一点,避免在程序运行过程中意外地改变INSTANCE的引用,从而破坏单例的唯一性。
代码验证
public class EagerSingletonTest {public static void main(String[] args) {A instance1 = A.getInstance();A instance2 = A.getInstance();// 验证两个实例是否为同一个对象System.out.println(instance1 == instance2); }
}
当你运行上述验证代码,如果输出结果为 true,就说明 instance1 和 instance2 实际上是同一个对象,也就证明了 A 类确实是单例的。
优缺点分析
- 优点:实现方式简单直接,而且天生具备线程安全性,无需额外的同步操作。由于类加载过程是由 JVM 保证线程安全的,所以在多线程环境下也不会出现创建多个实例的问题。
- 缺点:如果单例实例的创建过程比较耗时,或者会占用大量的系统资源,而在整个程序的运行过程中这个实例可能根本不会被使用,那么就会造成不必要的资源浪费。
2. 懒汉式单例(非线程安全)
原理
懒汉式单例则像是一个“拖延症患者”,它不会在类加载时就创建实例,而是等到第一次真正使用这个实例的时候才去创建。这种方式实现了延迟加载,避免了不必要的资源提前消耗。
示例代码
class B {// 声明一个静态变量,用于存储单例实例,但不立即初始化private static B INSTANCE;// 私有构造函数,防止外部通过 new 关键字创建实例private B() {}// 提供一个公共的静态方法,用于获取单例实例public static B getInstance() {if (INSTANCE == null) {INSTANCE = new B();}return INSTANCE;}
}
为什么使用 static 修饰 INSTANCE
同样,使用 static 修饰 INSTANCE 是为了确保它是一个全局唯一的变量,被所有 B 类的实例(虽然单例模式下通常不会有多个实例)共享。而且,getInstance() 方法是静态方法,静态方法只能访问静态变量,所以 INSTANCE 必须是静态的才能在 getInstance() 方法中被访问。
代码验证
public class LazySingletonNonThreadSafeTest {public static void main(String[] args) {B instance1 = B.getInstance();B instance2 = B.getInstance();// 验证两个实例是否为同一个对象System.out.println(instance1 == instance2); }
}
同样,运行上述验证代码,若输出为 true,则表明 B 类在单线程环境下是单例的。
优缺点分析
- 优点:实现了延迟加载,只有在真正需要使用实例的时候才会创建,避免了资源的提前浪费。
- 缺点:这种实现方式在多线程环境下是不安全的。想象一下,当多个线程同时进入
if (INSTANCE == null)这个条件判断语句时,可能会导致多个线程都认为INSTANCE为null,从而各自创建一个实例,破坏了单例的唯一性。
3. 懒汉式单例(线程安全,使用同步方法)
原理
为了解决懒汉式单例在多线程环境下的不安全问题,我们可以在 getInstance() 方法上添加 synchronized 关键字。这样一来,在多线程环境下,同一时间就只有一个线程能够进入这个方法,从而保证了单例的唯一性。
示例代码
class C {// 声明一个静态变量,用于存储单例实例,但不立即初始化private static C INSTANCE;// 私有构造函数,防止外部通过 new 关键字创建实例private C() {}// 使用 synchronized 关键字修饰方法,保证线程安全public static synchronized C getInstance() {if (INSTANCE == null) {INSTANCE = new C();}return INSTANCE;}
}
为什么使用 static 修饰 INSTANCE 和 getInstance() 方法
INSTANCE 使用 static 修饰的原因和前面一样,是为了保证它是全局唯一的变量。而 getInstance() 方法使用 static 修饰是因为我们希望通过类名直接调用这个方法来获取单例实例,而不需要创建类的实例。同时,由于 getInstance() 方法要访问静态变量 INSTANCE,所以它也必须是静态方法。
代码验证
public class LazySingletonThreadSafeTest {public static void main(String[] args) {C instance1 = C.getInstance();C instance2 = C.getInstance();// 验证两个实例是否为同一个对象System.out.println(instance1 == instance2); }
}
运行验证代码,输出 true 就说明 C 类在多线程环境下也是单例的。
优缺点分析
- 优点:保证了在多线程环境下的线程安全性,同时也实现了延迟加载。
- 缺点:由于每次调用
getInstance()方法都需要进行同步操作,这会带来一定的性能开销,尤其是在高并发的场景下,性能问题会更加明显。
4. 双重检查锁定单例
原理
双重检查锁定单例结合了懒汉式和同步机制的优点。它首先在不进行同步的情况下检查 INSTANCE 是否为 null,如果不为 null 则直接返回实例,这样可以减少同步的开销。只有当 INSTANCE 为 null 时,才会进行同步操作,并且在同步块内部再次检查 INSTANCE 是否为 null,以确保在多线程环境下不会创建多个实例。同时,为了避免指令重排序问题,需要使用 volatile 关键字修饰 INSTANCE 变量。
示例代码
class D {// 使用 volatile 关键字保证可见性,避免指令重排序private static volatile D INSTANCE;// 私有构造函数,防止外部通过 new 关键字创建实例private D() {}public static D getInstance() {if (INSTANCE == null) {synchronized (D.class) {if (INSTANCE == null) {INSTANCE = new D();}}}return INSTANCE;}
}
为什么使用 static 和 volatile 修饰 INSTANCE
static修饰的原因:和前面几种实现方式一样,使用static修饰INSTANCE是为了保证它是全局唯一的变量,被所有D类的实例共享。volatile修饰的原因:在 Java 中,指令重排序是指编译器和处理器为了提高性能,可能会对代码的执行顺序进行重新排序。在创建对象的过程中,可能会出现指令重排序的情况,导致INSTANCE引用在对象还未完全初始化时就被赋值。在多线程环境下,其他线程可能会看到一个未完全初始化的对象,从而引发错误。使用volatile关键字修饰INSTANCE可以禁止指令重排序,保证在多线程环境下的可见性和正确性。
代码验证
public class DoubleCheckedLockingSingletonTest {public static void main(String[] args) {D instance1 = D.getInstance();D instance2 = D.getInstance();// 验证两个实例是否为同一个对象System.out.println(instance1 == instance2); }
}
运行验证代码,若输出为 true,则证明 D 类是单例的。
优缺点分析
- 优点:既保证了线程安全,又实现了延迟加载,同时还减少了同步带来的性能开销,是一种比较优秀的实现方式。
- 缺点:实现相对复杂,需要开发者深入理解
volatile关键字和双重检查的原理。
5. 静态内部类单例
原理
静态内部类单例利用了 Java 静态内部类的特性。静态内部类在类加载时不会被加载,只有在第一次使用时才会被加载,并且类加载的过程是线程安全的。因此,这种方式既实现了延迟加载,又保证了线程安全。
示例代码
class E {// 私有构造函数,防止外部通过 new 关键字创建实例private E() {}// 静态内部类,包含一个静态常量 INSTANCE,用于存储单例实例private static class SingletonHolder {private static final E INSTANCE = new E();}// 提供一个公共的静态方法,用于获取单例实例public static E getInstance() {return SingletonHolder.INSTANCE;}
}
为什么内部类的 INSTANCE 使用 static final 修饰
static修饰的原因:使用static修饰INSTANCE是为了确保它是静态内部类SingletonHolder的静态成员,在类加载时就被初始化,并且被所有E类的实例共享。final修饰的原因:和前面一样,final修饰INSTANCE是为了保证它是一个不可变的引用,一旦指向了某个E类的实例,就不能再指向其他实例,从而保证单例的唯一性。
代码验证
public class StaticInnerClassSingletonTest {public static void main(String[] args) {E instance1 = E.getInstance();E instance2 = E.getInstance();// 验证两个实例是否为同一个对象System.out.println(instance1 == instance2); }
}
运行验证代码,输出 true 就表明 E 类是单例的。
优缺点分析
- 优点:线程安全,实现了延迟加载,代码简洁易懂,是一种比较推荐的实现方式。
- 缺点:需要开发者对 Java 静态内部类的加载机制有一定的了解。
6. 枚举单例
原理
Java 的枚举类型天生就是线程安全的,并且可以防止反序列化重新创建新的对象。因此,使用枚举来实现单例模式是一种非常简洁、高效且安全的方式。
示例代码
enum F {INSTANCE;public void doSomething() {System.out.println("Doing something...");}
}
枚举实现单例的优势
枚举类型在 Java 中是一种特殊的类,它的实例是有限且唯一的。在枚举类型中定义的枚举常量(如 INSTANCE)会在类加载时被创建,并且是线程安全的。同时,Java 的序列化机制对枚举类型有特殊的处理,反序列化时不会创建新的实例,从而保证了单例的唯一性。
代码验证
public class EnumSingletonTest {public static void main(String[] args) {F instance1 = F.INSTANCE;F instance2 = F.INSTANCE;// 验证两个实例是否为同一个对象System.out.println(instance1 == instance2); }
}
运行验证代码,若输出为 true,则说明 F 枚举类型实现了单例。
优缺点分析
- 优点:线程安全,防止反序列化重新创建新的对象,实现简单,是实现单例模式的最佳方式之一。
- 缺点:相对不够灵活,因为枚举类型默认继承
java.lang.Enum类,所以不能再继承其他类。
三、单例设计模式的使用场景
- 资源共享:在一些需要多个模块共享同一个资源的场景中,如数据库连接池、线程池等,使用单例模式可以确保资源的一致性和高效利用。因为多个实例可能会导致资源的冲突和浪费,而单例模式可以保证只有一个实例来管理这些资源。
- 配置管理:应用程序的配置信息通常只需要一个实例来管理。使用单例模式可以方便地获取和修改配置信息,避免了多个实例对配置信息的不一致修改。
- 日志记录:日志记录器通常是单例的,这样可以确保所有的日志信息都被记录到同一个地方,方便后续的查看和分析。如果有多个日志记录器实例,可能会导致日志信息分散,不利于管理。
四、单例设计模式的注意事项
序列化和反序列化问题
如果单例类实现了 Serializable 接口,在反序列化时可能会创建新的实例,从而破坏单例的唯一性。为了解决这个问题,需要重写 readResolve() 方法。
import java.io.ObjectStreamException;
import java.io.Serializable;class G implements Serializable {private static final G INSTANCE = new G();private G() {}public static G getInstance() {return INSTANCE;}// 重写 readResolve() 方法,防止反序列化创建新的实例private Object readResolve() throws ObjectStreamException {return INSTANCE;}
}
反射攻击问题
通过反射机制可以调用私有构造函数创建新的实例,这也会破坏单例的唯一性。为了防止反射攻击,可以在构造函数中添加判断逻辑。
class H {private static final H INSTANCE = new H();private static boolean isInstanceCreated = false;private H() {if (isInstanceCreated) {throw new IllegalStateException("Singleton instance already created!");}isInstanceCreated = true;}public static H getInstance() {return INSTANCE;}
}
五、总结
单例设计模式在 Java 开发中是一种非常实用的设计模式,它可以确保一个类只有一个实例,避免了资源的浪费和数据不一致的问题。在使用单例模式时,还需要注意序列化和反序列化、反射攻击等问题,确保单例的唯一性和安全性。
相关文章:
【Java进阶】java设计模式之单例模式
一、单例设计模式的基本概念 在 Java 编程的广阔天地里,单例设计模式宛如一颗璀璨的明星,是一种极为实用的创建型设计模式。它的核心使命是确保一个类在整个应用程序的生命周期内仅仅存在一个实例,并且为外界提供一个全局唯一的访问点来获取…...
AI编程界的集大成者——通义灵码AI程序员
一、引言 随着软件行业的快速发展和技术的进步,人工智能(AI)正在成为软件开发领域的一个重要组成部分。近年来,越来越多的AI辅助工具被引入到开发流程中,旨在提高效率、减少错误并加速创新。在这样的背景下࿰…...
第三十三:6.3. 【mitt】 任意组件通讯
概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。 // 引入mitt import mitt from "mitt";// 创建emitter const emitter mitt()/*// 绑定事件emitter.on(abc,(value)>{console.log(abc事件被触发,…...
6.7 数据库设计
文章目录 数据库设计6个阶段新奥尔良法完整导图 数据库设计6个阶段 数据库设计是指,根据应用环境,构造数据库模式,建立数据库、应用系统,实现有效地数据存储,以满足用户需求。 数据库设计过程包含6个阶段 数据库规划&…...
Java 大视界 -- Java 大数据在智能安防入侵检测与行为分析中的应用(108)
💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也…...
Vue3实现文件上传、下载及预览全流程详解(含完整接口调用)
文章目录 一、环境准备1.1 创建Vue3项目1.2 安装依赖1.3 配置Element Plus 二、文件上传实现2.1 基础上传组件2.2 自定义上传逻辑(Axios实现) 三、文件下载实现3.1 直接下载(已知文件URL)3.2 后端接口下载(二进制流&am…...
【云原生】SpringCloud-Spring Boot Starter使用测试
目录 Spring Boot Starter是什么? 以前传统的做法 使用 Spring Boot Starter 之后 starter 的理念: starter 的实现: ?创建Spring Boot Starter步骤 在idea新建一个starter项目、直接执行下一步即可生成项目。 ?在xml中加入如下配置…...
介绍下pdf打印工具类 JasperPrint
JasperPrint 工具类深度解析 JasperPrint 是 JasperReports 框架中实现 PDF 打印的核心载体类,其本质是 填充数据后的可打印报表对象,承担着从模板编译、数据填充到格式输出的全流程控制。以下从 7 个维度展开深度解析: 一、核心定位与生命周…...
idea中或pycharm中编写Markdown文件
参考 ltjt_aiseek: seek_backend_py 项目 数智科技ai探索API接口开发 1. 安装 Django 框架 在开始创建 Django 项目之前,需要先安装 Django 框架。可以通过 PyCharm 的终端或者系统的命令行工具来完成安装。 使用 PyCharm 终端安装 打开 PyCharm,如果…...
Go红队开发—并发编程
文章目录 并发编程go协程chan通道无缓冲通道有缓冲通道创建⽆缓冲和缓冲通道 等协程sync.WaitGroup同步Runtime包Gosched()Goexit() 区别 同步变量sync.Mutex互斥锁atomic原子变量 SelectTicker定时器控制并发数量核心机制 并发编程阶段练习重要的细节端口扫描股票监控 并发编程…...
使用自动化运维工具 Ansible 集中化管理服务器
一、概述 Ansible 是一款为类 Unix 系统开发的自由开源的配置和自动化工具 官方网站:https://www.ansible.com/ Ansible 成立于 2013 年,总部设在北卡罗来纳州达勒姆,联合创始人 ad Ziouani 和高级副总裁 Todd Barr都是红帽的老员工。Ansible 旗下的开源软件 Ansible 十分…...
数据集笔记:新加坡 一些交通的时间序列统计量
1 机动车年度保有量 data.gov.sg 各类机动车年度保有量 数据范围:2005年1月 - 2020年12月 1.1 数据说明 非高峰时段车辆 包括周末车(Weekend Cars)和 修订版非高峰时段车辆(Revised Off Peak Cars),该…...
企业jsapi_ticket,java举例
在企业微信开发中,使用 Java 获取 jsapi_ticket 并生成签名的步骤如下。以下是完整的 Java 示例代码。 1. 获取 jsapi_ticket 的流程 获取 access_token。 使用 access_token 获取 jsapi_ticket。 使用 jsapi_ticket 生成签名(signature)。…...
【FL0090】基于SSM和微信小程序的球馆预约系统
🧑💻博主介绍🧑💻 全网粉丝10W,CSDN全栈领域优质创作者,博客之星、掘金/知乎/b站/华为云/阿里云等平台优质作者、专注于Java、小程序/APP、python、大数据等技术领域和毕业项目实战,以及程序定制化开发…...
智能图像处理平台:图像处理配置类
这里我们先修改一下依赖,不用JavaCV,用openCV。 导入依赖: <!-- JavaCV 依赖,用于图像和视频处理 --> <!-- <dependency>--> <!-- <groupId>org.bytedeco</groupId>--> &l…...
《深度剖析:生成对抗网络中生成器与判别器的高效协作之道》
在人工智能的前沿领域,生成对抗网络(GAN)以其独特的对抗学习机制,为数据生成和处理带来了革命性的变革。生成器与判别器作为GAN的核心组件,它们之间的协作效率直接决定了GAN在图像生成、数据增强、风格迁移等众多应用中…...
【多模态大模型论文精读】MinMo语音交互大模型
写在前面:你需要一个更丝滑的语音助手 想象一下,你与一个语音助手对话,不再需要“嘿,Siri”或“小爱同学”这样的唤醒词,也不需要等待它一字一句地蹦出回复。你们可以像朋友一样,随时打断、插话,甚至同时说话。语音助手不仅能听懂你说了什么,还能理解你的语气、情感,…...
22-接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 方法一:双指针法 思路 使用两个指针 left 和 right 分别指向数组的两端,同时记录左边的最大高度 leftMax 和右边的最大高度 rig…...
如何长期保存数据(不包括云存储)最安全有效?
互联网各领域资料分享专区(不定期更新): Sheet 前言 这个问题需要考虑多个方面,比如存储介质的寿命、数据完整性、访问的便捷性,还有成本等因素。长期保存的话,存储介质的耐久性很重要。比如常见的硬盘、SSD、光盘、磁带等,各有优缺点。机械硬盘(HDD)的寿命一般在3-5年,…...
k8s拉取harbor镜像部署
在k8s中创建凭证 首先在节点docker登录harbor, 登录成功之后会在$HOME/.docker/ 生成一个config.json文件,这个就是登录凭证,后面docker pull就不需要再登录了。但是如果在k8s发布pod或者deploment时,这个凭证要在k8s中创建一个对…...
一周一个Unity小游戏2D反弹球游戏 - 球板的发球
前言 本文将实现当游戏开始时球在球板上,且不具备物理性,在Windows平台上通过点击屏幕来球发射,安卓平台上当手指触摸到屏幕上时进行发球,并此时开始具备物理性。 发球逻辑 首先在球板上创建一个球的发射点,新建一个空的游戏物体,并命名为BallPoint,并将其作为SpringBoa…...
C 语言共用体:深入理解与实践】
目录 一、引言 二、共用体的定义和基本语法 三、共用体的使用 3.1 声明共用体变量 3.2 给共用体成员赋值 3.3 共用体的内存布局 四、共用体的应用场景 4.1 节省内存空间 4.2 处理不同类型的数据 五、共用体使用的注意事项 六、总结 一、引言 在 C 语言中,共…...
Oracle性能调优(一):时间模型统计
Oracle性能调优(一):时间模型统计 时间模型统计视图时间模型统计指标时间模型统计视图 📖 DB Time的含义: DB Time表示前台会话在数据库调用中所花费的总时间,它是衡量数据库实例总负载的一个重要指标。DB Time是从实例启动时开始累计测量的,其计算方法是将所有前台会话…...
012 rocketmq事务消息
文章目录 事务消息概念介绍交互流程事务消息原理TransactionListener接⼝TransactionProducer.javaTransactionConsumer.java 事务消息 内置topic中的消息对消费者不可见 本地事务mq消息事务消息 消息队列 RocketMQ 版提供的分布式事务消息适⽤于所有对数据最终⼀致性有强需求…...
SpringBoot原理-02.自动配置-概述
一.自动配置 所谓自动配置,就是Spring容器启动后,一些配置类、bean对象就自动存入了IOC容器当中,而不需要我们手动声明,直接从IOC容器中引入即可。省去了繁琐的配置操作。 我们可以首先将spring项目启动起来,里面有一…...
知识图谱+智能问诊预诊系统vue+django+neo4j架构、带问诊历史
文章结尾部分有CSDN官方提供的学长 联系方式名片 文章结尾部分有CSDN官方提供的学长 联系方式名片 关注B站,有好处! 🤍编号:D032 🤍智能问答:智能问答自诊、预诊功能,同时可以保存问答历史 &…...
DeepSeek 助力 Vue3 开发:打造丝滑的悬浮按钮(Floating Action Button)
前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏关注哦 💕 目录 Deep…...
Java数据结构_一篇文章了解常用排序_8.1
本文所有排序举例均默认为升序排列。 目录 1. 常见的排序算法 2. 常见排序算法的实现 2.1 插入排序 2.1.1 基本思想: 2.1.2 直接插入排序 2.1.3 希尔排序(缩小增量排序) 2.2 选择排序 2.2.1 基本思想: 2.2.2 直接选择排…...
(南京观海微电子)——倍压设计与应用
在电路设计过程中,当后级需要的电压比前级高出数倍而所需要的电流并不是很大时,就可以使用倍压整流电路。倍压整流:可以将较低的交流电压,用耐压较高的整流二极管和电容器,“整”出一个较高的直流电压。 01 倍压整流电…...
AtCoder Beginner Contest AT_abc395_d ABC395D Pigeon Swap 题解
前言 在谎言中迷茫,试图躲避瓶颈。 可惜细节太多,浪费五发罚时。 一个绿名用户,被出题人卡住。 八十六分钟多,才看见一抹绿。 本题解 LaTeX \LaTeX LATEX 格式可能不太美观,以内容为主。 题目大意 有一群鸽子和它…...
