设计模式-单例模式

面向对象语言讲究的是万物皆对象。通常流程是先定义可实例化类,然后再通过各种不同的方式创建对象,因此类一般可以实例化出多个对象。但是实际项目开发时,我们还是希望保证项目运行时有且仅包含一个实例对象。这个需求场景的出发点包括但不限于以下几个方面:数据源对象(创建连接池、权限验证等均十分消耗资源)、线程池对象(同一个线程池)、日志对象(防止日志覆盖)等等。

一、单例模式概念

单例模式(Singleton Pattern)是指在一个系统应用中只有一个实例,并且是自行实例化的。其完整表述为:

Ensure a class has only one instance, and provide a global point of access to it.

Java单例模式是一种广泛使用的设计模式,单例模式有很多好处,他能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;

二、解决方案

为解决项目中有且仅存在唯一对象的需求,下面给出常见的几种单例模式的实现方案,并分析其优缺点。

2.1 饿汉式单例

饿汉式单例是指在项目启动加载类时进行初始化单例对象,并提供向外暴露单例对象的方法。代码如下:

/*** 饿汉式单例模式*/
public class HungrySingleton {// 类初始化时对象实例化private static HungrySingleton hungrySingleton = new HungrySingleton();// 构造器私有化private HungrySingleton() {}// 向外暴露获取单例对象的方法public static HungrySingleton getInstance() {return hungrySingleton;}
}

在这里插入图片描述
饿汉式单例模式特点:

  1. 单例类构造器私有化。(防止其他地方通过构造器创建对象)
  2. 单例对象初始化时机为类加载,并且是私有类成员变量
  3. 提供向外暴露单例对象方法

饿汉式单例模式优点:

  • 最简单的单例模式,执行效率最高
  • 线程安全(类加载时初始化)

虽然如此,但需要注意到,由于单例对象在类加载时即初始化完成,因此有可能此时初始化的对象并不会被业务逻辑使用,造成内存的浪费,而且无法被GC回收,造成内存利用率低。
因此,这种饿汉式单例模式的应用场景一般是单例对象被业务逻辑强依赖,即单例对象会被频繁的使用。

2.2 懒汉式单例

为解决饿汉式单例的对象浪费内存空间问题,对于不会被频繁使用的单例对象可以考虑使用懒汉式单例模式来初始化。懒汉式单例模式就是将对象初始化时机延迟到请求对象中,如果单例对象已经存在,直接返回,如果不存在单例对象,再进行初始化。初步代码如下:

/*** 懒汉式单例模式-非线程安全*/
public class LazySimpleSingleton {private static LazySimpleSingleton instance;private LazySimpleSingleton() {}// 向外暴露获取单例对象的方法public static LazySimpleSingleton getInstance() {if (instance == null) {     // 如果单例对象为null,则初始化单例对象instance = new LazySimpleSingleton();}return instance;}
}

类图和饿汉式基本无变化,区别仅在于单例对象的初始化时机。然而,饿汉式单例由于初始化是在类加载时进行,JVM已经保证了在多线程下只会被执行一次加载逻辑,但是懒汉式此例中无法保证在多线程下是否会被初始化多次,即产生了多个对象。
因此,获取单例对象时必须考虑线程安全问题。线程安全我们可以使用JVM提供的synchronized关键字修饰getInstance()保证获取实例对象方法时线程安全的,但是,这种方案也是存在问题,在高并发下,大量的请求同时需要获取单例对象时,就会由于同步锁竞争的原因使得服务性能变差。那还有没有其他方案既能保证线程安全,也能保证性能不受影响呢?答案是双重检查锁(Double Check Lock, DCL)。
双重检查锁大概描述为,第一重检查单例对象是否存在,存在就返回,不存在则进入synchronized同步代码块,在锁内部并进行第二重检查单例对象是否存在,存在即返回,不存在则初始化单例对象。这里面,首先第一重检查保证了大部分请求均不会收到锁的性能影响,第二重检查保证单例对象只会被初始化一次。实际代码如下:

/*** 懒汉式单例模式-双重检查锁*/
public class LazyDoubleCheckSingleton {private volatile static LazyDoubleCheckSingleton instance;private LazyDoubleCheckSingleton() {}public static LazyDoubleCheckSingleton getInstance() {if (instance == null) {     // 第一重检查:保证性能不受锁的影响synchronized (LazyDoubleCheckSingleton.class){  // 不存在单例对象时,才进入同步区if(instance == null) {      // 第二重检查:进入同步区再次检查保证确实不存在单例对象instance = new LazyDoubleCheckSingleton();}}}return instance;}
}

这种双重检查锁的单例实现方式就是既能够保证性能也能够保证延迟初始化对象的唯一性。需要说明的一点,你或许注意到了单例对象使用volatile关键字修饰,这是为什么呢?
首先volatile关键字有两个主要作用① 保证共享变量的可见性;② 禁止指令重排。前一个作用其实还好,因为synchronized本身就能够保证内存可见性,因此主要是由于第二个作用相关原因。继续我们再看new创建一个对象的实际过程主要分为三步:

  1. 分配对象内存空间
  2. 初始化对象
  3. 设置类成员变量执行分配的内存地址

cpu为了优化程序执行,可能会优化指令的执行顺序,如有可能第3步在第2步之前执行。如果线程A先执行了第3步,而没有执行第2步,此时CPU时间片轮转到线程B上,线程B会在第一次检查判断后返回单例对象,由于这里返回的对象实际并没有初始化完成,可能会导致线程B出现空指针或其他异常。
因此,使用volatile关键字修饰单例对象禁止CPU指令重拍优化,保证不会出现不成熟单例对象的出现和误用。

双重检查锁单例模式特点:

  1. 单例类构造器私有化。(防止其他地方通过构造器创建对象)
  2. 单例对象初始化时机为双重检查锁满足之后,并且是私有类成员变量,使用volatile修饰。
  3. 提供向外暴露单例对象方法

双重检查锁单例模式特点:

  • 对象懒加载,增加内存使用效率
  • 线程安全(双重检查锁+volatile关键字)

2.3 静态内部类单例

前面在使用双重检查锁的方式实现单例模式时,我们既要考虑性能、又要考虑线程安全,还需要考虑CPU指令重排的问题,实现起来相对十分复杂。并且另外一方面,synchronized同步锁在流量初期可能会出现性能尖刺问题,常常可能出现于线上发布器,也给系统稳定性带来了一定影响。有没有一种其他更好的方案呢?静态内部类单例可以算是一个答案。
如同饿汉式一样,静态内部类单例模式也还是利用JVM加载类能够保证线程安全的特性,但是又不能在单例类加载时就初始化单例对象,还是想这用到的时候才去初始化。解决这个问题的方法就是增加静态内部类,单例对象及其初始化维护在静态内部类中。而静态内部类的加载时机由getInstance()方法所决定。因此,静态内部类单例代码如下:

/*** 静态内部类单例模式*/
public class LazyStaticInnerClassSingleton {private LazyStaticInnerClassSingleton() {}public static LazyStaticInnerClassSingleton getInstance() {return LazyHolder.INSTANCE;     // 这里才会加载静态内部类LazyHolder}private static class LazyHolder {// 加载时会初始化单例对象private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();}
}

JVM将推迟LazyHolder的初始化操作,直到开始使用这个类时才初始化【因此,这也属于懒加载单例模式】,并且由于通过一个静态初始化来初始化单例对象,因此不需要额外的同步。当任何一个线程第一次调用getInstance时,都会使LazyHolder 被加载和被初始化,此时静态初始化器将执行单例对象的初始化操作。
静态内部类单例模式特点:

  1. 单例类构造器私有化。(防止其他地方通过构造器创建对象)
  2. 单例对象初始化时机为静态内部类加载
  3. 提供向外暴露单例对象方法

静态内部类单例模式特点:

  • 对象懒加载,增加内存使用效率
  • 线程安全(JVM保证)

2.4 枚举单例

枚举单例是《Effective Java》作者Joshua Bloch推荐使用的方式。以往的单例模式都有如下3个特点:

  1. 构造方法私有化
  2. 实例化的变量引用私有化
  3. 获取实例的方法共有

但是这种实现方式的问题就在于“私有化构造器并不保险”,因为私有构造方法仍然可以通过反射获取另外一个实例,继而破坏了单例模式。为了能保证在反射、序列化场景下创建对象的唯一性,因此我们可以借助枚举来实现单例模式【为什么枚举不能被序列化和反序列化?详见下一篇文章】。

public enum EnumSingleton {INSTANCE;public static EnumSingleton getInstance() {return INSTANCE;}
}

枚举单例模式实际上也类似于静态内部类单例,枚举单例模式属于饿汉式单例,内部枚举被JVM加载时会初始化单例对象。之所以使用枚举来加载单例对象,就是因为枚举能够防止用户通过反射、序列化等方式破坏对象的唯一性。
单例模式特点:

  1. 使用枚举对象作为单例对象
  2. 提供向外暴露单例对象方法

枚举单例模式特点:

  • 饿汉式加载
  • 线程安全(JVM保证)
  • 防止反射、序列化等破坏单例的场景

【参考资料】

  1. 一文带你搞定单例模式-知乎
  2. 单例模式详解-知乎(阿里巴巴)
  3. 请在1分钟内写一个线程安全的恶汉单例-JVM
  4. 为何用Enum枚举实现被认为是最好的方式

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

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

相关文章

变压器试验介质损耗

试验目的 介质损耗因数 tanδ (% ) 是判断变压器绝缘状态的一种较有效的手段, 主要用来检 查变压器整体受潮、 油质劣化及严重的局部缺陷等, 但不一定能发现变压器局部受潮 等集中性局部缺陷。 试验设备 异频介质损耗测试仪 厂家: 湖北众拓高试 试验接线 (1) 介…

搜索引擎elasticsearch :安装elasticsearch (包含安装组件kibana、IK分词器、部署es集群)

文章目录 安装elasticsearch1.部署单点es1.1.创建网络1.2.加载镜像1.3.运行 2.部署kibana2.1.部署2.2.DevTools2.3 分词问题(中文不友好) 3.安装IK分词器3.1.在线安装ik插件(较慢)3.2.离线安装ik插件(推荐)1)查看数据卷…

数据结构--哈夫曼树

数据结构–哈夫曼树 带权路径长度 结点的 权 \color{red}权 权:有某种现实含义的数值(如:表示结点的重要性等) 结点的带权路径长度 \color{red}结点的带权路径长度 结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积 树的…

小程序MobX创建store并实现全局数据共享

查看小程序根目录中是否存在package.json文件 在项目根目录运行cmd 没有package.json文件输入npm init -y初始化一下,初始化一个包管理 安装MobX npm install --save mobx-miniprogram4.13.2 mobx-miniprogram-bindings1.2.1 小程序菜单栏工具–构建npm 根目录创建store文…

el-ment ui 表格组件table实现列的动态插入功能

在实际需求中我们经常遇到各种奇葩的需求,不足为奇。每个项目的需求各不相同,实现功能的思路大致是一样的。 本文来具体介绍怎么实现table表格动态插入几列。 首先实现思路有2种, 1. 插入的位置如果是已知的,我知道在哪个标题的…

设计模式之建造者设计模式

写在前面 不知道,你在工作中有没有使用过lombok,如果你使用过,不知道你有没有使用过其中的Builder注解,其就会帮我们生成建造者设计模式相关的代码,本文就一起来看下吧! 1:介绍 1.1&#xff…

spark启动HA时workers为0,且为standby状态

今天学习一个spark视频,在启动StandAloneHa模式的时候,发现workers为0,而且spark两个master的状态都为standby,找了很久,才知道我用的spark3.2 最低支撑的zookeeper版本为3.5.x,而且zookeeper的安装包是需要带bin的那个…

CSS3 动画 animation 入门学习笔记 之 属性详解

文章目录 简单介绍 CSS 动画CSS 动画的作用CSS 动画语法介绍CSS 动画属性animation-nameanimation-durationanimation-delayanimation-directionanimation-iteration-countanimation-play-stateanimation-timing-functionanimation-fill-modeanimation 简单介绍 CSS 动画 引用…

基于深度学习的高精度80类动物目标检测系统(PyTorch+Pyside6+YOLOv5模型)

摘要:基于深度学习的高精度80类动物目标检测识别系统可用于日常生活中或野外来检测与定位80类动物目标,利用深度学习算法可实现图片、视频、摄像头等方式的80类动物目标检测识别,另外支持结果可视化与图片或视频检测结果的导出。本系统采用YO…

Matplotlib是什么

Matplotlib 是一款用于数据可视化的 Python 软件包,支持跨平台运行,它能够根据 NumPy ndarray 数组来绘制 2D 图像,它使用简单、代码清晰易懂,深受广大技术爱好者喜爱。 NumPy 是 Python 科学计算的软件包,ndarray 则…

HTTP1.1 wireshark分析

目录 http1.1wireshark分析http 1.1 keep-alive的2次http请求wireshark分析http1.1 keep-alive过期的2次请求keep-alive报文 本地springboot启动一个简单的服务,然后请求测试 tcpdump -i lo0 -nnvv -w tmp.cap tcpdump 本地回环网卡 http1.1 HTTP/1.0 每进行一次…

蚂蚁集团开源可信隐私计算框架「隐语」:开放、通用

7 月 4 日,蚂蚁集团宣布面向全球开发者正式开源可信隐私计算框架 “隐语”。 隐语是蚂蚁集团历时 6 年自主研发,以安全、开放为核心设计理念打造的可信隐私计算技术框架,涵盖了当前几乎所有主流隐私计算技术。 据介绍,隐语内置 MPC、TEE、同态等多种密态计算虚拟设备,提…

操作系统练习:创建内核模块,并加载和卸载模块

说明 本文记录如何创建和编译一个内核模块,以及加载和卸载内核模块。为《操作系统概念(第九版)》第二章,关于“Linux内核模块”的练习题。 创建内核模块 注:我这里是基于阿里云的轻量应用服务器(即当前博客服务器) 首…

【Linux】分布式监控 Zabbix

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 Zabbix 介绍zabbix 概述Zabbix 监控原理Zabbix 6.0 新特性Zabbix 6.0 功能组件 Zabbix 6.0 部署Zabbix 添加客户端主机Zabbix 自定义监控内容Zabbix 自动发现与自动…

Python+Requests+Excel接口测试实战

1、EXCEL文件接口保存方式,如图。 2、然后就是读取EXCEL文件中的数据方法,如下: 1 import xlrd2 3 4 class readExcel(object):5 def __init__(self, path):6 self.path path7 8 property9 def getSheet(self): 10 …

android更换开机动画

android11 路径:device / {vendor-name} / {platform-name} / {device-name} / system / bootanimation.zip 例:android \ device \ softwinner \ ceres \ ceres-b6 \ system \ bootanimation.zip android13 路径:device / softwinner / {PRO…

MyBatis全篇

文章目录 MyBatis特性下载持久化层技术对比 搭建MyBatis创建maven工程创建MyBatis的核心配置文件创建mapper接口创建MyBatis的映射文件测试功能加入log4j日志功能加入log4j的配置文件 核心配置文件的完善与详解MyBatis的增删改查测试功能 MyBatis获取参数值在IDEA中设置中配置文…

《TCP/IP网络编程》第3,4章学习记录

基础知识: struct sockaddr_in {sa_family_t sin_family; //地址族(Address Family)uint16_t sin_port; //16位TCP/UDP端口号struct in_addr sin_addr; //32位IP地址char sin_zero[8]; //不使用 }sa_family_t包括: (1)AF_INET,IPv4网络协议…

Linux宝塔Mysql读写分离配置,两台服务器,服务器存在多个库

Linux宝塔Mysql读写分离配置,两台服务器,服务器存在多个库 一、主库操作 #登录数据库,用root登录方便,用其他账号会提示权限不足,需要登录root给予权限 mysql -u root -p 密码#创建一个账号,供从库用该账…

大屏项目也不难

项目环境搭建 使用create-vue初始化项目 npm init vuelatest准备utils模块 业务背景:大屏项目属于后台项目的一个子项目,用户的token是共享的 后台项目 - token - cookie 大屏项目要以同样的方式把token获取到,然后拼接到axios的请求头中…