单例模式详解

article/2025/9/17 19:44:56

微信搜索【程序员囧辉】,关注这个坚持分享技术干货的程序员。

概述

单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,使用单例模式的类只有一个对象实例。

 

单例应用场景

  1. Windows系统的任务管理器。
  2. Windows系统的回收站。
  3. 操作系统的文件系统,一个操作系统只能有一个文件系统。
  4. 数据库连接池的设计与实现。
  5. 多线程的线程池设计与实现。
  6. Spring中创建的Bean实例默认都是单例。
  7. Java-Web中,一个Servlet类只有一个实例。
  8. 等等...

 

 

单例的实现要点

单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法)。

单例的实现主要是通过以下三个步骤:

  1. 将类的构造方法定义为私有方法。这样其他类的代码就无法通过调用该类的构造方法来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
  2. 定义一个私有的类的静态实例。
  3. 提供一个公有的获取实例的静态方法。

 

单例追求的目标

  1. 线程安全。
  2. 懒加载。
  3. 调用效率高。

 

单例模式的常用写法

常用的单例有以下5种写法,如果还有其他的写法,也基本是从以下5种稍微修改而来,由于内容基本一致,并且可能不是很常用,因此在本文中不再赘述。

 

1.饿汉模式

public class Singleton {// 1.饿汉private static Singleton instance = new Singleton();private Singleton() {}public static Singleton getInstance() {return instance;}
}

饿汉模式,比较常见的一种写法。在类加载的时候就对实例进行初始化,没有线程安全问题;获取实例的静态方法没有使用同步,调用效率高;但是没有使用懒加载,如果该实例从始至终都没被使用过,则会造成内存浪费。

 

总结:线程安全、非懒加载、效率高。

是否推荐:可以使用,但不推荐。

 

2.懒汉模式

public class Singleton {// 2.懒汉private static Singleton instance;private Singleton() {}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

线程安全的懒汉模式,比较常见的一种写法。在第一次使用的时候才进行初始化,达到了懒加载的效果;由于获取实例的静态方法用synchronized修饰,所以也没有线程安全的问题;但是,这种写法每次获取实例都要进行同步(加锁),因此效率较低,并且可能很多同步都是没必要的。

 

总结:线程安全、懒加载、效率低。

是否推荐:可以使用,但不推荐。

 

注:该模式还有另一种常见写法,就是把getInstance方法上的synchronized去掉,这种方法有线程安全问题,不能使用。

 

3.双重检测机制(DCL)

public class Singleton {// 3.双重检测机制private volatile static Singleton singleton;private Singleton() {}public static Singleton getSingleton() {if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {singleton = new Singleton();}}}return singleton;}
}

双重检测机制(双重检查加锁),比较常见的一种写法。在第一次使用的时候才进行初始化,达到了懒加载的效果;在进行初始化的时候会进行同步(加锁),因此没有线程安全问题;并且只有第一次进行初始化才进行同步,因此不会有效率方面的问题。

《Java Concurrency in Practice》作者Brian Goetz在书中提到关于DCL的观点:促使DCL模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在,因而它不是一种高效的优化措施。延迟初始化占位类模式(静态内部类)能带来同样的优势,并且更容易理解。

 

总结:线程安全、懒加载、效率高。

是否推荐:可以使用。

 

注:该模式还有另一种常见写法,就是把静态实例singleton的volatile修饰去掉,这种方法有线程安全方面的问题,不能使用。在我的另一篇文章有提到这个:volatile关键字详解,这里直接截取该部分内容,请见下面这个例子。

 

 

例子:双重检测机制实现单例(没有volatile修饰)

这段代码是单例的双重检测机制实现,相信很多人都用过,并且觉得这个代码是没问题的。在大多数情况,这段代码确实没问题,但在极端的情况下,有个隐藏的问题。

 

例子分析:

假设有两个线程同时访问这段代码,此时线程A走到15行开始初始化对象,线程B则刚走到12行进行第一次检测。这时要介绍下15行初始化这行代码,这行代码虽然只有一句话,但是被编译后会变成以下3条指令:

正常情况下,这3条执行时按顺序执行,双重检测机制就没有问题。但是CPU内部会在保证不影响最终结果的前提下对指令进行重新排序(不影响最终结果只是针对单线程,切记),指令重排的主要目的是为了提高效率。在本例中,如果这3条指令被重排成以下顺序:

如果线程A执行完1和3,instance对象还未完成初始化,但是已经不再指向null。此时线程B抢占到CPU资源,执行第12行的检测结果为false,则执行第19行,从而返回一个还未初始化完成的instance对象,从而出导致问题出现。要解决这个问题,只需要使用volatile关键字修饰instance对象即可。

 

4.静态内部类(延迟初始化占位类)

public class Singleton {// 4.静态内部类private static class Holder {private static final Singleton INSTANCE = new Singleton();}private Singleton() {}public static final Singleton getInstance() {return Holder.INSTANCE;}
}

静态内部类(延迟初始化占位类),比较常见的一种写法。JVM将推迟SingletonHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Singleton,因此不需要额外的同步。当任何一个线程第一次调用getInstance时,都会使SingletonHolder被加载和被初始化,此时静态初始化器将执行Singleton的初始化操作。

 

通过静态初始化来初始化Singleton为什么不需要额外的同步?

在初始器中采用了特殊的方式来处理静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据破坏。

 

总结:线程安全、懒加载、效率高。

是否推荐:推荐使用、《Java Concurrency in Practice》作者Brian Goetz推荐使用的方式。

 

5.枚举

public enum Singleton {INSTANCE;}

枚举,不是很常见的一种写法。很简洁的一种实现方式,提供了序列化机制,保证线程安全,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。

 

总结:线程安全、非懒加载、效率高。

是否推荐:推荐使用、《Effective Java》作者Joshua Bloch推荐使用的方式。

 

几种方式对比:

可能有人看了以上表格,觉得枚举有缺点,为什么Joshua Bloch还推荐使用枚举?

这就要提到单例的破解了。普通的单例模式是可以通过反射和序列化/反序列化来破解的,而Enum由于自身的特性问题,是无法破解的。当然,由于这种情况基本不会出现,因此我们在使用单例模式的时候也比较少考虑这个问题。

 

总结:

  1. 单例无论是在项目的开发中,还是面试中都是非常常见的,因此需要熟练的掌握好单例的知识。只要记住单例的三个实现要点——私有构造方法、定义静态Singleton实例、暴露实例获取方法,手写这5种实现方式相信都是信手拈来。如果是面试的时候,建议使用静态内部类的实现。
  2. 项目中具体使用哪种实现方式可以根据情况而定,个人推荐的方式是:静态内部类和枚举,我自己在项目中常见的也是这两种方式,可能静态内部类会更多一点。另外就是毕竟是两位大神推荐的,还考虑什么,直接用就是了。

—————END—————

 


http://chatgpt.dhexx.cn/article/FjCv35nW.shtml

相关文章

MATLAB中的结构体数组(struct)学习笔记

不要失却热情,不要丢掉冠军的心! MALAB中的结构体(struct)数组学习笔记 前言1. 版本2. 关键词 一、Struct结构体数组概述二、Struct结构体数组基本用法1. 结构体的创建2. 结构体中的筛选操作 前言 MATLAB中结构体数组基本用法笔…

MATLAB学习——结构体类型

前言 MATLAB 中的数据类型主要包括数值类型、 逻辑类型、 字符串、函数句柄、 结构体和单元数组类型。 这6种基本的数据类型都是按照数组形式存储和操作的。 一、结构体类型 MATLAB中的结构体与C语言中的结构体类似, 一个结构体可以通过字段存储多个不同类型的数…

Matlab将结构体struct字段内的数据转化到矩阵中

假设structure1,为一结构体,structure1.name为100个字符串 怎么将这些字符串不用循环一次性赋值到矩阵A?? Astructure1.name 为什么只是将第一个赋值过去? 答案是可以使用cat函数: 可以用cat函数, A cat(1,structur1.name)是…

Matlab:结构体数组

Matlab:结构体数组 创建标量结构体访问字段中的值对非标量结构体数组进行索引当您有要按名称组织的数据时,可以使用结构体来存储这些数据。结构体将数据存储在名为字段的容器中,然后您可以按指定的名称访问这些字段。使用圆点表示法创建、分配和访问结构体字段中的数据。如果…

Matlab-结构数组

1 认识结构数组 结构也是一种数据类型,它的每一个元素都有一个名字。称结构中的元素为域。 类似与C语言中的结构体。 2 创建 两种方法: (1)用赋值语句创建 (2)用函数 struct 函数进行创建 2.1 赋值语…

Matlab遇到结构体内容引用自非结构体数组对象

原因: 未初始化下一级结构体的值,如果直接调用就会报错。 举例: a.p[]; a.p.x解决方法: 需要先初始化,再调用 a.p.x[] or a.p.x0

Matlab:结构体Struct

Matlab中创建一个结构体数组的方式有两种,分别为直接引用方式和使用struct函数。 1、使用直接引用方式创建结构体 与建立数值型数组一样,建立新struct对象不需要事先申明,可以直接引用,而且可以动态扩充。比如建立一个复数变量x…

Matlab 结构体(struct)使用

转自http://blog.sina.com.cn/s/blog_468651400100c6c0.html 要在MALTAB中实现比较复杂的编程,就不能不用struct类型。而且在 MATLAB中实现struct比C中更为方便。 4. 3.1 结构数组的创建 MATLAB提供了两种定义结构的方式:直接应用和使用struct函数。 1. …

MATALB-结构体

结构体 结构体的生成一、 直接输入二、使用结构体生成函数struct 结构体的操作添加成员变量删除成员变量调用成员变量 结构体是另一种可以将不同类型数据组合在一起的数据类型。 MATLAB结构体变量类似于C语言结构体变量,且比C语音更加直观。 结构体与单元数组的区别为,结构体…

matlab学习-结构体变量

matlab结构体学习 matlab结构体学习 matlab结构体与C语言数据结构结构体或java语言类的定义有异曲同工之妙,其主要知识点如下 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 matlab结构体学习前言一…

matlab-结构体struct

在前面,有用到一个cell2struct()函数,cell是单元数组,而struct就是结构体。 1.建立结构体,在matlab中建立结构体有两种方式, eg:第一种为直接赋值 >> stu(1).namezhangsan;…

matlab中结构体使用方法

转自 http://hi.baidu.com/dess2211/blog/item/bb9b80185a7f940334fa417c.html matlab中使用结构体 2008-01-15 14:23 结构(struct)数组 要在MALTAB中实现比较复杂的编程,就不能不用struct类型。而且在 MATLAB中实现struct比C中更为方便。 4. 3.1 结构数组的创建 MA…

Matlab中结构体struct创建和使用

在项目上遇见了调用api接口,接口返回的信息为struct,故探讨一下matlab的struct结构体 1、struct结构体创建 创建结构体数组有两种方式,分别为直接创建和使用struct函数 1.1 直接创建 直接定义字段,像使用一般matlab变量一样&…

matlab 结构体

在前面,有用到一个cell2struct()函数,cell是单元数组,而struct就是结构体。 1.建立结构体,在matlab中建立结构体有两种方式, eg:第一种为直接赋值 >> stu(1).namezhangsan; …

单片机实例4——广告灯的左移右移(硬件电路图+汇编程序+C语言程序)

4. 广告灯的左移右移 1. 实验任务 做单一灯的左移右移,硬件电路如图4.4.1所示,八个发光二极管L1-L8分别接在单片机的P1.0-P1.7接口上,输出“0”时,发光二极管亮,开始时P…

蓝桥杯-左移右移(2022国赛)

蓝桥杯-左移右移 1、问题描述2、解题思路与代码实现2.1 方法一:使用LinkedList双向链表实现(50%)2.2 方法二:使用HashMap左右临界值实现(100%) 1、问题描述 小蓝有一个长度为 N 的数组, 初始时从左到右依次是 1,2,3,…N 。 之后小蓝对这个数组进行了 M 次…

第十三届蓝桥杯大赛软件类决赛Java大学B组C题——左移右移

【问题描述】 小蓝有一个长度为 N 的数组,初始时从左到右依次是 1, 2, 3, . . . N。 之后小蓝对这个数组进行了 M 次操作,每次操作可能是以下 2 种之一: 左移 x,即把 x 移动到最左边。右移 x,即把 x 移动到最右边。 …

算术逻辑左移右移

左移&#xff08;<<&#xff09;&#xff1a; 算术左移和逻辑左移的规则相同&#xff0c;都是整体左移&#xff0c;高位舍去&#xff0c;低位补零。 而且我们知道在计算机内部中的数都是用补码来进行存储的&#xff0c;这样便可以把减法运算也转化为加法运算。 1110的原码…

位运算符之左移右移(简单易懂)

前言: 位运算符是用来对二进制位进行操作的 c语言中有6种位运算符: & 按位与 [链接]: https://blog.csdn.net/weixin_42837024/article/details/98736834 | 按位或 [链接]:https://blog.csdn.net/weixin_42837024/article/details/98745019 ^ 按位异或 [链接]:https://blog…

关于左移右移的一些问题,左移右移之后应该应该赋值给原变量

关于左移右移的一些问题&#xff0c;左移右移之后应该应该赋值给原变量 欢迎使用Markdown编辑器 你好&#xff01; 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章&#xff0c;了解一下Markdown的基本语法知识。…