单例模式五种写法

单例模式五种写法

单例模式有五种写法:饿汉、懒汉、双重检验锁、静态内部类、枚举.

单例模式属于设计模式中的创建型模式

一、单例模式应用场景

  • windows的task manager(任务管理器)就是很典型的单例模式;

  • windows的recycle bin(回收站)也是典型的单例应用,在整个系统运行过程中,回收站一直维护仅有的一个实例;

  • 项目中,读取配置文件的类,一般也只有一个对象,没有必要每次使用配置文件数据,每次new一个对象去读取;

  • 应用程序的日志应用,一般都可用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;

  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源;

  • 在spring中,每个bean默认就是单例的,这样做的优点是spring容器可以管理;

  • 在servlet编程中,每个servlet也是单例;

  • 在spring mvc框架,控制器对象也是单例;

单例模式算是设计模式中最容易理解,也是最容易手写代码的模式了。但是其中的坑却不少,所以也常作为面试题来考,本文主要对几种单例写法的整理,并分析其优缺点。

二、合格的单例模式要求

合格的单例模式的实现,至少要保证以下三点:

1. 实现单例功能;
2. 延迟加载;
3. 并发时不出错。 

三、五种单例模式的实现

我们将创建一个 SingleObject 类。SingleObject 类有它的私有构造函数和本身的一个静态实例。

SingleObject 类提供了一个静态方法,供外界获取它的静态实例。SingletonPatternDemo 类使用 SingleObject 类来获取 SingleObject 对象。

image-20240408234756088

1. 饿汉式

这种方式非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的

public class Singleton {private static final Singleton instance = new Singleton();private Singleton() {}public static Singleton getInstance() {return instance;}
}
  • 饿汉模式的缺点:可能在还不需要此实例的时候就已经把实例创建出来了,没起到 lazy loading 效果,空间换时间:浪费内存。
  • 饿汉模式的优点:实现简单,线程安全,调用效率高。

2. 懒汉式

public class Singleton {private static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
  • 懒汉模式的优点:弥补了饿汉模式的缺点,起到了 lazy loading 的效果,时间换空间:节约内存。
  • 懒汉模式的缺点:多线程并发时有线程安全问题,有可能创建多个实例,也就是说在多线程下不能正常工作。

3. 双重检验锁

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。

称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。

public class Singleton {private static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { //第一次检查,提高性能,避免所有的线程都去加锁和释放锁synchronized (Singleton.class) {if (instance == null) { //第二次检查,确保只有一个线程创建实例instance = new Singleton();}}}return instance;}
}

为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了

这段代码看起来很完美,很可惜,它是有问题的。**主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情: **

(1) 给 instance 分配内存(2) 调用 Singleton 的构造函数来初始化成员变量(3) 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器(JIT)中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。 如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

解决办法: 我们只需要将 instance 变量声明成 volatile 修饰就可以了,其作用是防止指令重排序

public class Singleton {private volatile static Singleton instance; //声明成 volatileprivate Singleton (){}public static Singleton getInstance() {if (instance == null) {                         synchronized (Singleton.class) {if (instance == null) {       instance = new Singleton();}}}return instance;} 
}

使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作

  • 双校验懒汉模式的优点:弥补了懒汉模式的缺点,防止了并发问题。
  • 双校验懒汉模式的缺点:因为涉及到锁,因此性能有损耗;代码变得更复杂。

4. 静态内部类-登记式

静态内部类(static nested class)

使用静态内部类的方式,也是《Effective Java》上所推荐的。

public class Singleton {  private static class SingletonHolder {  private static final Singleton INSTANCE = new Singleton();  }  private Singleton (){}  public static final Singleton getInstance() {  return SingletonHolder.INSTANCE; }  
}

懒汉式的变种: 静态内部类 空间换时间

首先静态内部类不会随着外部类的加载而加载 ,只有静态内部类的静态成员被调用时才会进行加载也就是初始化 ,这个就是调用静态内部类的静态成员,然后在初始化的过程中new一个对象INSTANCE

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的,同时读取实例的时候不会进行同步,没有性能缺陷;

  • 静态内部类单例的优点:线程安全,支持延迟加载,获取singleton对象时不需要加锁,使用方便。
  • 静态内部类单例的缺点:会增加类的数量,在第一次加载时可能会略微增加启动时间。

5. 枚举

用枚举写单例非常简单,这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法:

public enum EasySingleton{INSTANCE;
}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。

枚举单例优点:写法简单,实现效率高,因为枚举本身就是单例,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞

枚举单例缺点:没有延迟加载,枚举实例在编译时就已经创建,无法在运行时延迟加载

四、总结

一般来说,单例模式有五种写法:饿汉、懒汉、双重检验锁、静态内部类、枚举

一般情况下,不建议使用第 2 种懒汉方式,建议使用第 1 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。如果有其他特殊的需求,可以考虑使用第 3 种双检锁方式。

  • 单例类的特点:
(1)单例类确保自己只有一个实例(2)单例类必须自己创建自己的实例(3)单例类必须为其他对象提供唯一的实例。
  • 单例类的优点:
(1) 控制资源的使用,通过线程同步来控制资源的并发访问。(2) 控制实例的产生数量,达到节约资源的目的。(3) 作为通信的媒介,数据共享。他可以在不建立直接关联的条件下,让多个不相关的两个线程或者多个进程之间实现通信。
  • 单例模式的主要缺点如下:
(1) 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。(2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,
提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。

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

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

相关文章

ENVI实战—一文学会使用传感器自带信息配准工具进行几何校正

实验1:学会使用传感器自带信息配准工具 目的:利用ENVI的传感器自带信息配准工具,掌握几何校正的一般方法。 过程: 1.对MODIS影像进行校正: ①读取影像:打开文件,点击“打开为”,…

Docker安装xxl-job分布式任务调度平台

文章目录 Docker安装xxl-job分布式任务调度平台1.xxl-job介绍2. 初始化“调度数据库”3、docker挂载运行xxl-job容器3.1、在linux的opt目录下创建xxl_job文件夹,并在里面创建logs文件夹和application.properties文件3.2、配置application.properties文件&#xff0c…

Python对txt文本文件内容进行替换,以便通过Origin进行数据分析

因为要使用Origin进行数据分析,数据集为单行文本逗号隔开,无法直接复制粘贴到Origin中,故为此整理了一下代码,方便后续直接使用。 一、任务需求 有个1.txt文档文件里面是一行数据信息,要将其规整为每行一个数据&…

Spring学习(二)

图解: 2.核心容器总结 2.2.1 容器相关 BeanFactory是IoC容器的顶层接口,初始化BeanFactory对象时,加载的bean延迟加载 ApplicationContext接口是Spring容器的核心接口,初始化时bean立即加载 ApplicationContext接口提供基础的be…

Linux:编译器 - gcc

Linux:编译器 - gcc gcc概述语言发展史gcc的编译过程预处理编译汇编 gcc的链接过程动态库与静态库 gcc概述 GCC(英文全拼:GNU Compiler Collection)是 GNU 工具链的主要组成部分,是一套以 GPL 和 LGPL 许可证发布的程…

Web APP设计:将多个相近的机器学习模型融合到一个Web APP中

将多个相近的机器学习模型融合到一个Web APP中 FUSE-ML是一个用于预测腰椎融合术后效果的APP,它可以做出三个不同的结论,分别评价术后的腰痛、腿痛和日常功能是否提高。 这估计是部署了三个机器学习模型在这个APP中,因为一个机器学习模型仅…

【Java开发指南 | 第四篇】Java常量、自动类型转换、修饰符

读者可订阅专栏:Java开发指南 |【CSDN秋说】 文章目录 Java常量自动类型转换Java修饰符 Java常量 常量在程序运行时是不能被修改的。 在 Java 中使用 final 关键字来修饰常量,声明方式和变量类似: final double PI 3.1415927;自动类型转换…

【解读】《中华人民共和国网络安全法》:所有IT从业者都应知应懂

随着网络的快速发展,当今社会存在的网络安全问题也是接踵而来:网络入侵、网络攻击等非法活动威胁信息安全;非法获取公民信息、侵犯知识产权、损害公民合法利益;宣扬恐怖主义、极端主义,严重危害国家安全和社会公共利益…

电机控制器电路板布局布线参考指导(五)

电机控制器电路板布局布线参考指导(五)大容量电容和旁路电容的放置 1.大容量电容的放置2.电荷泵电容器3.旁路电容/去耦电容的放置3.1 靠近电源3.2 靠近功率器件3.3 靠近开关电流源3.4 靠近电流感测放大器3.5 靠近稳压器 tips:资料主要来自网络…

如何把音频转视频?MP3转换成MP4怎么操作?快来和小编一起学习吧

小伙伴们都知道mp3是最常用的音频文件格式,而mp4是最常用的视频文件格式。有时候为了方便mp3和mp4文件的时候,可能需要将mp3文件转换成mp4视频格式,遇到这种情况时候,很多小伙伴却不知道如何操作。今天小编就为大家介绍2个简单的方…

对桥接模式的理解

目录 一、背景二、桥接模式的demo1、类型A(形状类型)2、类型B(颜色类型)3、需求:类型A要使用类型B(如:红色的方形)4、Spring的方式 一、背景 在《对装饰器模式的理解》中&#xff0…

OpenHarmony南向开发实例:【游戏手柄】

介绍 基于TS扩展的声明式开发范式编程语言,以及OpenHarmony的分布式能力实现的一个手柄游戏。 完成本篇Codelab需要两台开发板,一台开发板作为游戏端,一台开发板作为手柄端,实现如下功能: 游戏端呈现飞机移动、发射…