一、单例设计模式的基本概念

在 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,就说明 instance1instance2 实际上是同一个对象,也就证明了 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) 这个条件判断语句时,可能会导致多个线程都认为 INSTANCEnull,从而各自创建一个实例,破坏了单例的唯一性。

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 修饰 INSTANCEgetInstance() 方法

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 则直接返回实例,这样可以减少同步的开销。只有当 INSTANCEnull 时,才会进行同步操作,并且在同步块内部再次检查 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;}
}
为什么使用 staticvolatile 修饰 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 开发中是一种非常实用的设计模式,它可以确保一个类只有一个实例,避免了资源的浪费和数据不一致的问题。在使用单例模式时,还需要注意序列化和反序列化、反射攻击等问题,确保单例的唯一性和安全性。

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

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

相关文章

011 rocketmq过滤消息

文章目录 过滤消息TAG模式过滤FilterByTagProducer.javaFilterByTagConsumer.java SQL表达式过滤FilterBySQLProducer.javaFilterBySQLConsumer.java 类过滤模式(基于4.2.0版本) 过滤消息 消息过滤包括基于表达式过滤与基于类模式两种过滤模式。其中表达…

【心得】一文梳理高频面试题 HTTP 1.0/HTTP 1.1/HTTP 2.0/HTTP 3.0的区别并附加记忆方法

面试时很容易遇到的一个问题—— HTTP 1.0/HTTP 1.1/HTTP 2.0/HTTP 3.0的区别,其实这四个版本的发展实际上是一环扣一环的,是逐步完善的,本文希望帮助读者梳理清楚各个版本之间的区别,并且给出当前各个版本的应用情况,…

大模型部署与调优:从基础到高效优化全解析

大模型部署与调优:从基础到高效优化全解析 1. 引言 随着深度学习的快速发展,大模型(Large Models) 在自然语言处理(NLP)、计算机视觉(CV)、推荐系统等领域的应用日益广泛。然而&am…

小红书app复制短链,分享链接转直接可访问链接

简介:小红书手机app分享的链接需要点击才能获取完成链接,本文教大家如何通过代码的方式将xhs的短连接转化为长链接。 1.正常我们分享的链接是这样的: 44 小猪吃宵夜发布了一篇小红书笔记,快来看吧! 😆 KeA…

DeepSeek 助力 Vue3 开发:打造丝滑的弹性布局(Flexbox)

前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏关注哦 💕 目录 Deep…

DeepSeek开源周Day5压轴登场:3FS与Smallpond,能否终结AI数据瓶颈之争?

2025年2月28日,DeepSeek开源周迎来了第五天,也是本次活动的收官之日。自2月24日启动以来,DeepSeek团队以每天一个开源项目的节奏,陆续向全球开发者展示了他们在人工智能基础设施领域的最新成果。今天,他们发布了Fire-F…

SQL AnyWhere 的备份与恢复

目录 一、备份 二、恢复 1、自动恢复 2、映像恢复 3、日志恢复-指定时间点 4、日志恢复-指定偏移 5、完整的恢复流程 6、恢复最佳实践 三、其他操作 1、dbtran 2、SQL Shell 工具 数据库的安装与基本使用内容请参考博客: SAP SQLAnyWhere 17 的安装与基本使用_sql…

入门基础项目(SpringBoot+Vue)

文章目录 1. css布局相关2. JS3. Vue 脚手架搭建4. ElementUI4.1 引入ElementUI4.2 首页4.2.1 整体框架4.2.2 Aside-logo4.2.3 Aside-菜单4.2.4 Header-左侧4.2.5 Header-右侧4.2.6 iconfont 自定义图标4.2.7 完整代码 4.3 封装前后端交互工具 axios4.3.1 安装 axios4.3.2 /src…

unity学习61:UI布局layout

目录 1 布局 layout 1.1 先准备测试UI,新增这样一组 panel 和 image 1.2 新增 vertical layout 1.3 现在移动任意一个image 都会影响其他 1.3.1 对比 如果没有这个,就会是覆盖效果了 1.3.2 对比 如果没有这个,就会是覆盖效果了 1.4 总结&#xf…

翻译: 深入分析LLMs like ChatGPT 一

大家好,我想做这个视频已经有一段时间了。这是一个全面但面向普通观众的介绍,介绍像ChatGPT这样的大型语言模型。我希望通过这个视频让大家对这种工具的工作原理有一些概念性的理解。 首先,我们来谈谈你在这个文本框里输入内容并点击回车后背…

Ubuntu 下 nginx-1.24.0 源码分析 - ngx_conf_add_dump

ngx_conf_add_dump 定义在src\core\ngx_conf_file.c static ngx_int_t ngx_conf_add_dump(ngx_conf_t *cf, ngx_str_t *filename) {off_t size;u_char *p;uint32_t hash;ngx_buf_t *buf;ngx_str_node_t *sn;ngx_conf_dump_t *cd;has…

Oracle 导出所有表索引的创建语句

在Oracle数据库中,导出所有表的索引创建语句通常涉及到使用数据字典视图来查询索引的定义,然后生成对应的SQL语句。你可以通过查询DBA_INDEXES或USER_INDEXES视图(取决于你的权限和需求)来获取这些信息。 使用DBA_INDEXES视图 如…

快速搭建多语言网站的 FastAdmin 实践

快速搭建多语言网站的 FastAdmin 实践 引言 在全球化的背景下,越来越多的网站需要支持多种语言,以便满足不同用户的需求。FastAdmin 是一个基于 ThinkPHP 的快速后台开发框架,提供了丰富的功能和灵活的扩展性,非常适合用于快速搭…

Python 实战:构建分布式文件存储系统全解析

Python 实战:构建分布式文件存储系统全解析 在当今数据爆炸的时代,分布式文件存储系统凭借其高可扩展性、高可靠性等优势,成为了数据存储领域的热门选择。本文将详细介绍如何使用 Python 构建一个简单的分布式文件存储系统。从系统架构设计&…

【综合项目】api系统——基于Node.js、express、mysql等技术

目录 0 前言 1 初始化 2 注册登录 2.1 注册 2.1.1 功能:密码加密(2.3.3) 2.1.1.1 操作 2.1.1.2 bcryptjs详解 2.1.2 插入新用户(2.3.4) 2.1.3 优化:表单数据验证(2.5) …

tableau之标靶图、甘特图和瀑布图

一、标靶图 概念 标靶图(Bullet Chart)是一种用于显示数据与目标之间关系的可视化图表,常用于业务和管理报告中。其设计旨在用来比较实际值与目标值,同时展示额外的上下文信息(如趋势)。 作用 可视化目标…

Linux下的网络通信编程

在不同主机之间,进行进程间的通信。 1解决主机之间硬件的互通 2.解决主机之间软件的互通. 3.IP地址:来区分不同的主机(软件地址) 4.MAC地址:硬件地址 5.端口号:区分同一主机上的不同应用进程 网络协议…

网络七层模型—OSI参考模型详解

网络七层模型:OSI参考模型详解 引言 在网络通信的世界中,OSI(Open Systems Interconnection)参考模型是一个基础且核心的概念。它由国际标准化组织(ISO)于1984年提出,旨在为不同厂商的设备和应…

530 Login fail. A secure connection is requiered(such as ssl)-java发送QQ邮箱(简单配置)

由于cs的csdN许多文章关于这方面的都是vip文章,而本文是免费的,希望广大网友觉得有帮助的可以多点赞和关注! QQ邮箱授权码到这里去开启 授权码是16位的字母,填入下面的mail.setting里面的pass里面 # 邮件服务器的SMTP地址 host…

Sqlserver安全篇之_TLS的证书概念

证书的理解 参考Sqlserver的官方文档https://learn.microsoft.com/zh-cn/sql/database-engine/configure-windows/certificate-overview?viewsql-server-ver16 TLS(Transport Layer Security)传输层安全和SSL(Secure Sockets Layer)安全套接字层协议位于应用程序协议层和TCP/…