多线程——单例模式

目录

·前言

一、设计模式

二、饿汉模式

三、懒汉模式

1.单线程版

2.多线程版

·结尾


·前言

        前面的几篇文章中介绍了多线程编程的基础知识,在本篇文章开始,就会利用前面的多线程编程知识来编写一些代码案例,从而使大家可以更好的理解运用多线程来编写程序,本篇文章会用多线程来实现设计模式中的“单例模式”,这里实现“单例模式”的方式主要介绍两种:“饿汉模式”和“懒汉模式”,下面进行本篇文章的重点内容吧。

一、设计模式

        本篇文章介绍的单例模式属于设计模式中的一种,那么什么是设计模式呢?设计模式和象棋中的“棋谱“”比较类似,比如“红方当头炮,黑方马来跳”,针对红方的一些走法,黑方应招也有一些固定的套路,按照这种套路来下,局势就不会吃亏,按照棋谱下棋,下出来的棋不会太差,因为棋谱会兜住我们下棋的下限,设计模式也是如此,按照设计模式来写代码同样可以兜住我们的下限。

        单例模式,是设计模式的一种,它可以保证某个类在程序中只存在唯一的一份实例,而不会创建出多个实例,这点需求在很多场景都需要,比如在我们前面 MySql 篇章 JDBC 编程中的 DataSource 实例就只需要一个。使用单例模式,就可以对我们的代码进行一个更严格的校验和检查,不会像口头约定那样还可以创建多个实例。

        单例模式的具体实现有很多种,本篇文章就来介绍两种实现方式:“饿汉模式”和“懒汉模式”。

二、饿汉模式

        饿汉模式下实现的单例模式,在类加载时就会创建好对象实例,具体的代码已经运行示例如下所示,通过代码中的注释对代码再进一步介绍:

// 希望这个类在进程中只有一个实例
class Singleton{private static Singleton instance = new Singleton();// get 方法设为静态方法,这样其他代码想要使用这个类的实例就需要通过这个方法来获取// 不应该在其他代码中重新 new 这个对象,而是使用这个方法获取现成的对象public static Singleton getInstance() {return instance;}// 将构造方法设为 private 这样其他代码中就无法通过构造方法再进行实例化一个新对象private Singleton() {}
}public class ThreadDemo1 {public static void main(String[] args) {// 利用"饿汉模式"实现的单例模式创建两个对象,观察这两个对象是否相同Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1==s2);}
}

        上述的代码就是“饿汉模式”单例模式中一种简单的实现方式,这里实例是在类加载的时候就创建了,创建的时机非常早,这就相当于程序一启动,实例就创建好了,就使用“饿汉”来形容“创建实例非常迫切,非常早”。

三、懒汉模式

        懒汉模式下实现的单例模式,在类加载的时候不创建实例,在第一次使用的时候才创建实例。这样的设计方式可以节省一些不必要的开销,以生活中的肯德基疯狂星期四为例,只有在星期四时,肯德基的点餐小程序上才会出现疯狂星期四的特价餐品,此时使用懒汉模式,不是星期四时就不会加载疯狂星期四的特价餐品,就会节省一些开销。

1.单线程版

        下面来以懒汉模式来实现一个单线程版的单例模式,示例代码及运行结果如下所示:

// 懒汉模式---单线程版
class SingletonLazy{// 这个引用指向唯一实例,初始化为 null,而不是立即创建实例private static SingletonLazy instance = null;private SingletonLazy() {}public static SingletonLazy getInstance() {if (instance == null){// 首次调用 getInstance 方法,创建实例instance = new SingletonLazy();}// 如果不是第一次调用 getInstance 方法,直接返回之前创建好的实例return instance;}
}public class ThreadDemo2 {public static void main(String[] args) {// 利用"懒汉模式"实现的单例模式创建两个对象,观察这两个对象是否相同SingletonLazy b = SingletonLazy.getInstance();SingletonLazy b2 = SingletonLazy.getInstance();System.out.println(b==b2);}
}

        由运行结果可以看出,上述的代码写法仍然可以保证该类的实例是唯一一个,与此同时,创建实例的时机就不是程序启动时了,而是第一次调用 getInstance 方法的时候。 

2.多线程版

        通过上面单线程版的懒汉模式实现单例模式,我们可以来分析一下上述的代码是否是线程安全的呢?结论一定是不安全的,不然也不会再创建一个多线程版的懒汉模式实现单例模式,那么以上代码在哪里会涉及到线程安全问题呢?这里出现问题的核心代码就是 getInstance 方法,下面通过画图的方式来对这里的线程安全问题进行讲解:

        如上图所示,在线程 t1 判断完成,当前是第一次执行 getInstance 方法后进入 if 语句内,没等创建实例就被调度走去执行线程 t2 ,此时 t2 虽然是第二次调用 getInstance 方法,但是由于线程 t1 调用 getInstance 方法还没有创建实例,所以线程 t2 执行 if 语句显示 instance 仍然为 null,此时线程 t2 开始创建实例,并返回实例,然后又跳转回线程 t1 ,t1 继续执行创建实例,这时,该进程中就会出现两个实例,也就出现了线程安全问题。  

        如何改进单线程的懒汉模式,使它也成为线程安全的代码呢?这就需要我们进行加锁操作,想要使这里的代码执行正确,其实只需把 if 和 创建实例的两个操作打包成原子的(不可拆分),这样就可以解决单线程的懒汉模式中的线程安全的问题,加锁逻辑如下图所示:

        如上图两个线程在加锁后的执行流程所示,此时就可以确保,一定是 t1 执行完实例(new)操作修改了 instance 之后再回到 t2 执行 if 语句了,这时 if 的条件就不会成立了,t2 就会直接返回 instance 了。

        但是这样加锁之后还有一个问题,如果 instance 已经创建过实例了,此时后续再调用 getInstance 方法就都是直接返回 instance 实例了,这时调用 getInstance 方法就属于纯粹的读操作了,就不会有线程安全问题了,不过,按照上图中的代码逻辑,即使创建完 instance 实例后是线程安全的代码,仍然每次调用都会先加锁再释放锁,此时效率就会变低(加锁意味着产生阻塞,一旦阻塞解除时间就不确定了)。

        为了解决上述加锁引入的新问题,我们可以在每次加锁前再进行一次判断,仍然是判断当前 instance 的值是否为 null ,为 null 就继续加锁,不为 null 就可以直接返回 instance 对象,不用再进行加锁操作了,具体代码如下图所示:

        如上图所示的代码中,synchronized 上下两条 if 语句中判断的内容是一样的,这里虽然 if 中进行的判断相同,但是所判断的含义还是有所差别:

  1. 第一个 if 判断当前是否要加锁;
  2. 第二个 if 判断的是当前是否要创建实例 

        上面代码很凑巧的 if 中的判断条件相同了,但是一个是为了保证“线程安全”一个是“保证“执行效率”,这也就形成了双重校验锁。

        代码改到此处,还是存在一个问题,那就是由指令重排序引起的线程安全问题,指令重排序是一种编译器的优化方式,调整原有的代码执行顺序,保证逻辑不变的前提下提高程序的效率,但是在多线程中,这种优化就很可能带来线程安全问题,上面代码中,创建 instance 实例的过程就很可能会被指令重排序,创建 instance 实例代码如下:

instance = new SingletonLazy();

        上面这段代码,可以拆分成三个大的步骤:

  1. 申请一段内存空间;
  2. 在这个内存空间上调用构造方法,创建出这个实例;
  3. 把这个内存地址赋值给 instance 引用变量。 

        正常的情况下,会按 1,2,3 的顺序来执行上面这段代码,但是编译器可能会将上面代码优化成 1,3,2 的顺序来执行,这时就可能会出现问题,如下图所示的情况: 

        如上图的线程调度过程,t2 线程执行完 getInstance 方法后得到的是一个各个属性都未初始化“全0”值的 instance 实例,此时如果使用 t2 线程如果使用了 instance 里面的属性或者方法就会出现错误。

        这种错误出现的原因是由于线程 t1 在创建实例执行完了 1,3 后,被调度走,此时 instance 指向的是一个非 null 的,但是未初始化的对象,这时 t2 线程就会判定 instance==null 不成立,直接 return ,得到一个各个属性都未初始“全0”值的 instance 实例,此时使用这个实例就会出现问题,但是如果创建实例的代码按照 1,2,3 的顺序来执行,就不会出现上述的问题了,所以解决这个问题的方法就是阻止编译器对这段代码的指令重排序,这就需要使用到我们前面文章介绍的关键字 volatile 了。

        这里还是再介绍一下 volatile 关键字的功能把,主要有两个:

  1. 保证内存可见性:每次访问变量都必须要重新读取内存,而不会优化成到寄存器或缓存中读取变量;
  2. 禁止指令重排序:针对这个 volatile 关键字修饰的变量的读写操作相关指令是不能被重排序的。 

        代码中需要进行指令重排序的地方是为 instance 创建实例的时候,所以我们可以直接针对这个变量加上 volatile 关键字进行修饰,这样,针对这个变量再进行读写操作就不会出现重排序了,此时,创建实例的顺序一定是 1,2,3 也就预防了上述的问题。

        代码修改到这里就算没有问题了,那么正确懒汉模式实现单例模式多线程版的代码就可以写出来了,代码及一些详细注释如下所示:

// 懒汉模式---多线程版
class SingletonLazy{// 这个引用指向唯一实例,初始化为 null,而不是立即创建实例private volatile static SingletonLazy instance = null;private static Object locker = new Object();private SingletonLazy() {}public static SingletonLazy getInstance() {// 如果 instance 为 null, 说明是首次调用,首次调用就需要考虑线程安全问题,需要加锁if (instance == null) {synchronized (locker) {if (instance == null){// 首次调用 getInstance 方法,创建实例instance = new SingletonLazy();}}}// 如果不是第一次调用 getInstance 方法,直接返回之前创建好的实例return instance;}
}public class ThreadDemo2 {public static void main(String[] args) {// 利用"懒汉模式"实现的单例模式创建两个对象,观察这两个对象是否相同SingletonLazy b = SingletonLazy.getInstance();SingletonLazy b2 = SingletonLazy.getInstance();System.out.println(b==b2);}
}

·结尾

        文章到这里就要结束了,本篇文章利用前面文章介绍的多线程基础知识来实现了一个小案例——单例模式的实现,这里介绍的两种实现方式:饿汉模式与懒汉模式,由于饿汉模式从类加载时就已经创建好实例,后续获取实例都是读操作不涉及线程安全问题,所以饿汉模式下的单例模式代码天生就是线程安全的,反观,懒汉模式在多线程与单线程下就有很大的差别了,此时单线程版的懒汉模式在多线程中就会引发线程安全问题,上面文章详细介绍了每个会出现线程安全问题的地方,希望能够给大家讲解清楚,最后在基于单线程版的懒汉模式代码下,修改出了多线程版的懒汉模式代码,理解清楚这里相信会对你理解线程安全问题有很大的帮助,如果对文章哪里感到疑惑,欢迎在评论区进行留言讨论哦~我们下一篇文章再见~~~

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

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

相关文章

Cypress安装用命令安装

安装node 试一下,安装yarn 用命令安装Cypress 下面找个截图说:会给用给几个用例引导你怎么写测试脚本

阿里云 EMR Serverless Spark 版正式开启商业化

阿里云 EMR Serverless Spark 版已于2024年9月14日正式商业化售卖,本文将简要介绍 EMR Serverless Spark 的产品优势、应用场景、支持地域,及计费模式等。 EMR Serverless Spark 是一款云原生,专为大规模数据处理和分析而设计的全托管 Server…

基于JSP实习管理系统【附源码】

基于SSM的学生管理系统(源码L文说明文档) 目录 4 系统设计 4.1 系统概述 4.2系统功能结构设计 4.3数据库设计 4.3.1数据库E-R图设计 4.3.2 数据库表结构设计 5 系统实现 5.1管理员功能介绍 5.1.1管理员登录 5.1.2…

数字身份管理建设是传统社会向数字社会演进的核心关键

当前,新一轮科技革命和产业变革突飞猛进。科学技术尤其是以互联网、大数据、云计算、人工智能和区块链等为代表的数字技术正与社会交往、社会服务、社区建设、社会治理等领域不断渗透融合,社会正在由人与环境构成的物理关系总和向“万物数字化”和万物互…

重磅!望繁信科技与德勤中国签署战略合作协议

2022年,望繁信科技与德勤中国签署流程挖掘战略合作协议!双方强强联合,在拓展流程优化市场、推动企业数智融合等领域展开深度合作,持续共建具有全球影响力的流程挖掘新生态。 根据协议内容,双方计划在未来三年内&#x…

软考攻略/超详细/系统集成项目管理工程师/基础知识分享18

6.5数据分析及应用 6.5.1 数据集成(掌握) 数据集成就是将驻留在不同数据源中的数据进行整合,向用户提供统一的数据视图,使得用户能以透明的方式访问数据。 WebServices技术是一个面向访问的分布式计算模型,它的本质是…

RabbitMQ 入门(六)SpringAMQP五种消息类型(Direct Exchange)

一、发布订阅-DirectExchange(路由模式) 在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。 Direct Exchan…

关键链项目管理是什么?它如何优化传统项目管理?

在项目管理的世界里,方法论千千万万,但真正能够提升项目效率和成功率的却并不多见。关键链项目管理(Critical Chain Project Management, CCPM)作为一种独特且高效的管理方式,正在被越来越多的企业所采用。相较于传统的…

NAND 数据恢复:使用 VNR 闪存数据恢复软件提取闪存转储中的块

天津鸿萌科贸发展有限公司从事数据安全服务二十余年,致力于为各领域客户提供专业的数据恢复、数据备份解决方案与服务,并针对企业面临的数据安全风险,提供专业的相关数据安全培训。 天津鸿萌科贸发展有限公司是专业 NAND 闪存数据恢复工具 VN…

linux下离线安装jq工具

故障现象: 当前使用的是CentOS7, 使用sudo yum install jq这个命令后,总是报错 Loaded plugins: fastestmirror, langpacks Determining fastest mirrors ... Cannot find a valid baseurl for repo: extras/7/x86_64 使用uname -a查看我当…

Yolov10训练的餐盘菜品目标检测软件(包含源码及数据集)

本文摘要 摘要:本文主要使用YOLOV10深度学习框架自训练了一个“餐盘菜品目标检测模型”,基于此模型使用PYQT5实现了一款界面软件用于功能演示。让您可以更好的了解和学习,该软件支持图片、视频以及摄像头进行目标检测,本系统所涉…

gitlab项目转移群组

1、背景 项目pa不再使用,只需要备份代码就行。将项目pa从A群组转移到B群组。 2、转移 在群组A项目pa中,设置-通用-高级-转移项目

Linux 线程概念及线程控制

1.线程与进程的关系 执行流(Execution Flow)通常指的是程序执行过程中的控制路径,它描述了程序从开始到结束的指令执行顺序。例如我们要有两个执行流来分别进行加法和减法的运算,我们可以通过使用 fork 函数来创建子进程&#xf…

全面了解 NGINX 的负载均衡算法

NGINX 提供多种负载均衡方法,以应对不同的流量分发需求。常用的算法包括:最少连接、最短时间、通用哈希、随机算法和 IP 哈希。这些负载均衡算法都通过独立指令来定义,每种算法都有其独特的应用场景。 以下负载均衡方法(IP 哈希除…

置分辨率设置多显示器的时候提示, 某些设置由系统管理员进行管理

遇到的问题 设置分辨率设置多显示器的时候提示(如下图所示): 某些设置由系统管理员进行管理 解决方法 先试试这个方法: https://answers.microsoft.com/zh-hans/windows/forum/all/%E6%9B%B4%E6%94%B9%E5%88%86%E8%BE%A8%E7%8…

拓森空调计费系统

随着现代建筑技术的不断发展,中央空调系统已经成为许多大型建筑、商场、办公楼等场所的必备设施。为了更有效地管理和控制中央空调的使用,同时实现能源的合理分配和费用的精确计算,空调计费系统应运而生。 空调计费系统是一种用于精确计算每个…

Java时区国际化解决方案

当用户所在时区和服务器所在时区不一致时,会产生时区相关问题,如时间显示错误、程序取得的时间和数据库存储的时间不一致、定时任务的触发没有跟随用户当前的时区等等问题. 统一拦截时区 /*****/ Component Slf4j public class TimeZoneIdInterceptor implements HandlerInte…

前端开发设计模式——状态模式

目录 一、状态模式的定义和特点 二、状态模式的结构与原理 1.结构: 2.原理: 三、状态模式的实现方式 四、状态模式的使用场景 1.按钮的不同状态: 2.页面加载状态: 3.用户登录状态: 五、状态模式的优点 1.提…

RabbitMQ 入门(七)SpringAMQP五种消息类型(Topic Exchange)

一、Topic Exchange(消息模式) TopicExchange 与DirectExchange类似,区别在于routingKey可以是多个单词的列表,并且以.分割。 Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过…

数据结构与算法——Java实现 42.二叉树的最大深度

苦尽甘来时,一路向阳开 —— 24.10.21 104. 二叉树的最大深度 给定一个二叉树 root ,返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1: 输入:root [3,9,20,null,null,15,7] 输出&a…