设计模式之单例模式及其在多线程下的使用

很多时候,我们在使用类创建类的实例并不想可以创建很多实例对象,比如在数据库连接的时候,对于一个数据库的连接通常只需要连接池中的某个连接的实例,连接一次即可,对于session会话,用户在访问网页做会话保持的时候,一个用户只需要一个实例来表示本次会话即可。

设计模式就像是下围棋那样的一些定式,棋谱,如果对方小飞挂角,我们可以选择小飞守角,或者大飞守角等等,也就说如果一个棋力不足的新手能把一部分常用的定式或者棋谱背下来,那么遇到了类似的情况或者定式的招数就可以运用出来,而不至于下的一塌糊涂,也就是为新手菜狗提供了保底的手法。

1.单例模式

在23种典型的设计模式中就存在一种单例模式,可以很好的解决我们最初提到的,"全局单个实例"的场景,就是"单例模式"很有见名知意的感觉。

1.1 定义

单例模式在大佬给出的定义是:

通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的办法。

1.提供全局变量使得一个对象被访问是什么意思呢?

MyClass instance = new MyClass(); // 每次都可以创建新的实例

可以把instance设置为一个全局变量,比如

private static final MyClass instance = new MyClass();

 2.让类自身负责保存它唯一的实例是什么意思?

这个类做到两件事:

  1. 私有化构造方法,别人就不能随便 new 它了;

  2. 在类中自己创建并保存唯一的实例

  3. 提供一个公开的静态方法用于获取该实例

1.2 单例模式的使用

在单例模式这个定式中,存在两种手法

1.饿汉式的单例模式

饿汉式就是,在提供全局变量的时候,就为这个全局变量创建一个实例,这个全局变量和实例一般设置为static,这样他就会随着类的加载就进行初始化。

public class Singleton {// 1. 提前创建好唯一实例(类加载时就实例化)private static final Singleton instance = new Singleton();// 2. 构造方法私有化,防止外部 newprivate Singleton() {}// 3. 提供静态方法让外部访问实例public static Singleton getInstance() {return instance;}
}
2.懒汉式的单例模式

懒汉式就是,在提供全局变量的时候并不为这个全局变量创建实例,而是等到使用时在提供的全局访问点,也就是提供的静态方法去获得实例的时候,在进行创建实例,这样就减少了类加载时的一些初始化工作。

public class Singleton {private static Singleton instance;private Singleton() {}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton(); // 延迟加载}return instance;}
}

单例模式除了可以保证唯一的实例外,还有什么好处呢?

比如单例模式因为Singleton类封装他的唯一实例,这样它可以严格控制客户怎么样访问它以及合适访问它,简单来说就是对唯一实例的受控访问。

单例模式通过自己管理自己的唯一实例(比如通过 private static Singleton instance),并只提供一个公开的获取方法(如 getInstance(),从而实现对这个实例的访问控制

  • 外部不能随便 new 一个新的对象;

  • 外部必须通过你提供的方式来访问;

  • 类本身可以在需要的时候控制创建时机(比如懒汉式延迟创建);

这就叫“对唯一实例的受控访问”。

单例模式看起来有点像实用类中的静态方法,比如Math类有很多数学计算方法,他们之间虽然很类似,实用类通常也会采用私有化的构造方法来避免其有实例。但是他们还是有很多不同的

单例类和工具类(实用类)在结构上是有点像的,比如:

  • 都私有了构造方法,不允许 new

  • 都通过类名来访问功能(方法或实例);

比如 Math.abs(-1) 这样的调用方式也不用创建对象,看起来就和 Singleton.getInstance() 类似。

1.实用类不保存状态,仅提供一些静态方法或者静态属性来让我们使用,单例模式却是有状态的。

2.实用类不能用于继承多态,而单例模式虽然实例唯一,却可以有子类来继承

3.实用类只不过是一些方法属性的集合,而单例模式确实有着唯一的对象实例。

2.多线程下的单例模式

很多代码程序在单线程下运行的十分完美,但是到了多线程的环境下就会暴露出很多短板甚至是bug,比如上面的单例模式,在多个线程同时,注意是同时访问Singleton类,调用getInstance方法是会有可能创建多个实例的。

很尴尬,那应该怎么解决呢?

线程安全问题的发现与解决-CSDN博客

我们在前面分析了,多线程下的线程安全问题,这种情况就属于线程安全的问题之一,

是因为,修改操作不是原子的情况所造成的

比如下面的代码

/*** 懒汉式单例模式*/
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy(){};public static SingletonLazy getInstance(){if(instance == null){instance = new SingletonLazy();}return instance;}
}

我们发现,在getInstance中并没有像之前提到的count++这样的修改操作呀

也就是有个 

if(instance == null)//判断
instance = new SingletonLazy();//赋值
return instance;

返回操作是一种"读操作"通常不会是多线程下bug的元凶

那么原因是因为if(instance == null)//判断 或者 instance = new SingletonLazy();//赋值 再或者是二者合并起来造成的问题吗?

Java中的赋值操作,确实本质上是一种"读操作"也不应该是造成问题的原因,

原因是第三种情况,拆开各自安好,合并就可能会出现问题了,因为if(instance == null)//判断 和instance = new SingletonLazy();//赋值 二者放在一起是一个完整的逻辑。

1.多线程改进1

 问题核心就是线程不安全导致重复实例化

我们不妨尝试一下加锁,让不是原子性的操作变成加锁后的原子性的操作

synchronized (SingletonLazy.class){if(instance == null){instance = new SingletonLazy();}}

之前我们讨论解决线程安全时讲过synchronized的使用

在此处的getInstance方法中,想要加锁因为是静态方法的缘故,就要使用当前类的Class对象来充当锁对象。

如果想要使用实例的锁对象也是可以的可以这样写代码:

/*** 懒汉式单例模式*/
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy(){};private static final Object lock = new Object();public static SingletonLazy getInstance(){synchronized (lock){if(instance == null){instance = new SingletonLazy();}}return instance;}
}

关于锁的问题,我们不再讨论,现在我们来看一下加锁后的效果

  1. 线程1 抢到了锁,成功进入 synchronized (lock) 的同步块;

  2. 线程1 执行判断:instance == null,结果为 true

  3. 线程1 开始创建单例对象(执行 new SingletonLazyO());

  4. 此时线程2 也调用了 getInstance(),但因为同步块已经被线程1占用,所以线程2在 synchronized 外面等待

  5. 线程1 创建完实例后,退出同步块,释放了锁,并且 instance 已经指向新建好的对象;

  6. 线程2 被唤醒,获取到了锁,进入同步块;

  7. 再次检查 instance == null,这次结果为 false(因为线程1已经创建好了);

  8. 线程2 直接返回现有的实例,避免了重复创建

  • 最终,两个线程都获得了同一个对象实例;

  • 没有出现重复创建或资源浪费的问题;

  • 符合单例模式“全局唯一实例”的设计目标;

  • 这种方式虽然线程安全,但每次访问都进入同步块,性能稍差,可以通过双重检查优化

2.多线程改进2

我们知道加锁是存在一定的代价的

为了避免每次都进入 synchronized 块,可以使用“双重检查锁”: 

public static SingletonLazy getInstance(){if(instance == null){synchronized (lock){if(instance == null){instance = new SingletonLazy();}}}return instance;}

初次见这种双重if而且内外if条件还是相同的,很多新手会觉得代码逻辑很混乱

if (instance == null) — 第一次检查(不加锁

  • 这是性能优化的关键:

    • 大多数时候,instance 已经被创建了,不需要进入同步代码块;

    • 只有第一次创建的时候才需要同步;

    • 避免每次都加锁,提高效率


synchronized (Singleton.class)

  • 加锁的对象是类的 .class 对象,因为 instance 是静态变量,是整个类共享的;

  • 保证只有一个线程可以创建实例

  • 是解决线程安全的核心。


if (instance == null) — 第二次检查(加锁后再确认

  • 为什么要检查两次?

    • 如果不再判断一遍,多个线程可能都在排队等锁;

    • 第一个线程创建了对象,释放锁后,后面的线程仍然会再创建一次,如果不检查;

    • 所以要加锁后再检查一次,防止重复创建。


instance = new Singleton();

  • 真正创建对象的地方;

  • 只有在加锁的前提下,并且确认 instance 为 null 的时候才会执行。

3.多线程改进3

上面的代码仍然存在一定的缺陷,我们还有一种很隐匿的缺陷没有找到,那就是指令重排序的问题

线程安全问题的发现与解决-CSDN博客

前面我们提到,线程安全的几大问题其中之一就是,修改操作不是原子的

new SingletonLazy()

这条语句看起来仅仅只是Java的一条普通的实例化语句,但是在JVM层面就包括了三个步骤

1.为该实例开辟内存空间,分配内存

2.初始化该实例对象

3.最后instance引用赋值,引用这一块内存空间

编译器会觉得,如果我快点引用,先不初始化能不能让代码执行的更快,更高效呢?

所以它大胆的调换了执行顺序变成了

1.为该实例开辟内存空间,分配内存

3.instance引用赋值,引用这一块内存空间

2.初始化该实例对象

这不换不要紧,一换的话,如果存在别的线程在“赋值”和“初始化之间”访问这个对象,顺便修改了就会造成bug。

假如线程 A 执行到 instance = new SingletonLazy();,由于重排序:

  • 它已经把 instance 指向了还“没初始化”的对象

  • 此时线程 B 也进来了,看到 instance != null,以为已经初始化好了

  • 然后就直接拿这个对象用了(return instance)!结果呢?对象状态是不完整的!

这就产生了严重的**“半初始化对象被访问”**的问题

这里其实也说明了为什么单线程下指令重排序根本没有问题

因为

不存在别的线程在“赋值”和“初始化之间”访问这个对象,也就不存在bug。

3.多线程下单例模式的使用总结

综上,很多代码在单线程下生龙活虎,因为单线程下没有其他线程来“抢时间”、“抢资源”,所以很多细节(比如原子性、可见性、重排序)根本不会暴露出来。这就是为什么并发编程下会出现很多的问题,所以我们在使用单例模式的多线程版本的时候,要记得以下两点

  • 使用双重 if 判定(Double-Checked Locking)
    避免每次获取实例都加锁,提高性能。

  • 在实例变量上添加 volatile 关键字
    防止 JVM 发生指令重排序,确保对象初始化的完整性。

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

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

相关文章

Apache Ignite 2.8 引入的新指标系统(New Metrics System)的完整说明

这段文档是关于 Apache Ignite 2.8 引入的“新指标系统(New Metrics System)” 的完整说明。这是 Ignite 监控体系的一次重大升级,相比旧的、分散的统计方式,新系统更统一、灵活、可扩展。 我们来逐层拆解、通俗易懂地理解这个新…

【氮化镓】GaN同质外延p-i-n二极管中星形与三角形扩展表面缺陷的电子特性

2025年7月23日,美国国家标准与技术研究院(NIST)与美国海军研究实验室的Andrew J. Winchester等人在《Applied Physics Letters》期刊发表了题为《Electronic properties of extended surface defects in homoepitaxial GaN diodes》的文章,基于光电发射电子显微术、导电原子…

使用 Scrapy 框架定制爬虫中间件接入淘宝 API 采集商品数据

一、引言 在电商数据分析、市场调研等领域,获取淘宝平台上的商品数据是一项常见需求。淘宝提供了 API 接口,允许开发者通过授权的方式获取商品信息。本文将介绍如何使用 Scrapy 框架定制爬虫中间件,实现对淘宝 API 的接入,从而高…

Jmeter全局变量跨线程组的使用

一、线程组1中从数据库中查询到字段值二、BeanShell取样器中设置为全局变量#为什么说props.put("Out1",Out);其实是设置Out1为Jmeter的属性了呢? 因为在后面的调试取样器运行结果中,会发现如果只打开显示变量开关,是看不到Out1运行…

前端技术栈详解

前端技术栈是指构建现代Web应用程序所需的一系列技术和工具的集合。以下是当前主流前端技术栈的详细解析&#xff1a; 一、核心基础技术 1. HTML5 作用&#xff1a;网页内容的结构化标记关键特性&#xff1a; 语义化标签&#xff08;<header>, <section>, <arti…

Git Pull 时遇到 Apply 和 Abort 选项?详解它们的含义与应对策略

在使用 Git 进行团队协作时&#xff0c;git pull 是最常用的命令之一&#xff0c;用于拉取远程仓库的最新代码并合并到本地分支。但有时执行 git pull 后&#xff0c;Git 会提示 ​Apply&#xff08;应用&#xff09;​​ 和 ​Abort&#xff08;中止&#xff09;​​ 两个选项…

暑期算法训练.11

目录 47. 力扣203 移除链表元素 47.1 题目解析&#xff1a; ​编辑 47.2 算法思路&#xff1a; 47.3 代码演示&#xff1a; ​编辑 48. 力扣2.两数相加 48.1 题目解析&#xff1a; ​编辑 48.2 算法思路; 48.3 代码演示&#xff1a; 48.4 总结反思&#xff1a; …

nl2sql grpo强化学习训练,加大数据量和轮数后,准确率没提升,反而下降了,如何调整

在NL2SQL任务中使用GRPO强化学习训练时&#xff0c;增加数据量和训练轮数后准确率下降&#xff0c;通常是由过拟合、训练不稳定、奖励函数设计不合理、数据质量问题或探索-利用失衡等原因导致的。以下是具体的诊断思路和调整策略&#xff0c;帮助定位问题并优化性能&#xff1a…

PHP/Java/Python实现:如何有效防止恶意文件上传

文章目录 木马病毒防范:文件上传如何彻底防止伪造文件类型 引言 一、文件类型伪造的原理与危害 1.1 常见伪造手段 1.2 潜在危害 二、防御体系设计 2.1 防御架构 三、核心防御技术实现 3.1 服务端验证实现 3.1.1 文件内容检测(Python示例) 3.1.2 扩展名与内容双重验证(Java示…

SpringBoot系列之基于Redis的分布式限流器

SpringBoot系列之基于Redis的分布式限流器 SpringBoot 系列之基于 Redis 的分布式限流器 图文并茂,代码即拷即用,支持 4 种算法(固定窗口 / 滑动窗口 / 令牌桶 / 漏桶) 一、为什么要用分布式限流? 单机 Guava-RateLimiter 在集群下会 各玩各的,流量漂移,无法全局控量。…

面试遇到的问题2

Redisson的看门狗相关问题 首先要明确一点&#xff0c;看门狗机制的使用方式是&#xff1a;在加锁的时候不加任何参数&#xff0c;也就是&#xff1a; RLock lock redisson.getLock("myLock"); try {lock.lock(); // 阻塞式加锁// 业务逻辑... } finally {lock.unl…

Linux—进程概念与理解

目录 1.冯诺依曼体系结构 小结&#xff1a; 2.操作系统 概念&#xff1a; 结构示意图&#xff1a; 理解操作系统&#xff1a; 用户使用底层硬件层次图&#xff1a;​编辑 3.进程 概念 结构示意图 task_ struct内容分类 典型用法示例 观察进程: 了解 PID PPID 查…

LeetCode 面试经典 150_数组/字符串_买卖股票的最佳时机(7_121_C++_简单)(贪心)

LeetCode 面试经典 150_数组/字符串_买卖股票的最佳时机&#xff08;7_121_C_简单&#xff09;题目描述&#xff1a;输入输出样例&#xff1a;题解&#xff1a;解题思路&#xff1a;思路一&#xff08;贪心算法&#xff09;&#xff1a;代码实现代码实现&#xff08;思路一&…

Ubuntu 18.04 repo sync报错:line 0: Bad configuration option: setenv

repo sync时报 line 0: Bad configuration option: setenv因为 Ubuntu 18.04 默认的 openssh-client 是 7.6p1&#xff0c;还不支持 setenv&#xff0c;但是.repo/repo/ssh.py 脚本中明确地传入了 SetEnv 参数给 ssh&#xff0c;而你的 OpenSSH 7.6 不支持这个参数。需要按如下…

bug记录-stylelint

BUG1不支持Vue文件内联style样式解决&#xff1a; "no-invalid-position-declaration": null

前端开发(HTML,CSS,VUE,JS)从入门到精通!第一天(HTML5)

一、HTML5 简介1&#xff0e;HTML全称是 Hyber Text Markup Language&#xff0c;超文本标记语言&#xff0c;它是互联网上应用最广泛的标记语言&#xff0c;简单说&#xff0c;HTML 页面就等于“普通文本HTML标记&#xff08;HTML标签&#xff09;“。2&#xff0e;HTML 总共经…

智慧收银系统开发进销存:便利店、水果店、建材与家居行业的—仙盟创梦IDE

在数字化转型的浪潮中&#xff0c;收银系统已不再局限于简单的收款功能&#xff0c;而是成为企业进销存管理的核心枢纽。从便利店的快消品管理到建材家居行业的大宗商品调度&#xff0c;现代收银系统通过智能化技术重塑了传统商业模式。本文将深入探讨收银系统在不同行业进销存…

三维扫描相机:工业自动化的智慧之眼——迁移科技赋能智能制造新纪元

在当今工业4.0时代&#xff0c;自动化技术正重塑生产流程&#xff0c;而核心工具如三维扫描相机已成为关键驱动力。作为工业自动化领域的“智慧之眼”&#xff0c;三维扫描相机通过高精度三维重建能力&#xff0c;解决了传统制造中的效率瓶颈和精度痛点。迁移科技&#xff0c;自…

Jmeter的元件使用介绍:(九)监听器详解

监听器主要是用来监听脚本执行的取样器结果。Jmeter的默认监听器有&#xff1a;查看结果树、聚合报告、汇总报告、用表格查看结果&#xff0c;断言结果、图形结果、Beanshell监听器、JSR223监听器、比较断言可视化器、后端监听器、邮件观察器&#xff0c;本文介绍最常用的监听器…

联通元景万悟 开源,抢先体验!!!

简介&#xff1a; 元景万悟智能体平台是一款面向企业级场景的一站式、商用license友好的智能体开发平台&#xff0c;是业界第一款go语言&#xff08;后端&#xff09;开发的智能体开发平台&#xff08;7月19日&#xff09;&#xff0c;coze studio开源是7月26日&#xff0c;同时…