多线程——单例模式

目录

·前言

一、设计模式

二、饿汉模式

三、懒汉模式

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.xdnf.cn/news/19668.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

关于Web Component

2024年8月14日 引言 Web Component 是一种用于构建可复用用户界面组件的技术,开发者可以创建自定义的 HTML 标签,并将其封装为包含逻辑和样式的独立组件,从而在任何 Web 应用中重复使用,并且可以做到无框架跨框架。 不同于 Vue…

【进阶系列】python的模块

模块 创建一个 .py 文件,这个文件就称之为 一个模块 Module 如何使用 import 想要B.py文件中,使用A.py文件,只需要在B.py文件中使用关键字import导入即可。 import A# 若A是一个包的话,可以这样写 import A.函数名from impor…

全志T113双核异构处理器的使用基于Tina Linux5.0——RTOS编译开发说明

3、RTOS编译开发说明 3.1、RTOS SDK与TinaLinux开发环境 RTOS SDK相关代码已集成到Tina Linux开发环境,Tina Linux开发环境下的rtos子目录即为RTOS开发环境。 ├──brandy ├──bsp ├──build ├──buildroot ├──build.sh >build/top_build.sh ├──…

十六.SpringCloudAlibaba极简入门-整合Grpc代替OpenFeign

前言 他来了他来了,停了快2个月了终于又开始更新文章啦,这次带来的绝对是干货!!!。由于公司项目进行重构的时候考虑到,OpenFeign做为服务通信组件在高并发情况下有一定的性能瓶颈,所以将其替换…

【Linux】环境变量详解

Linux环境变量 1.环境变量分类2.环境变量相关指令3.常用的环境变量4.环境变量的组织方式5.获取环境变量6.命令行参数 1.环境变量分类 按生命周期划分: 永久的:在环境变量脚本文件中配置,用户每次登录时会自动执行这些脚本,相当于永…

SpringBoot项目搭建IEDA2023.1.2

导入依赖 ——————————————————

L0G1000 Linux基础知识(包含ssh报错处理)

1.vscode通过ssh链接云服务器 按教程https://github.com/InternLM/Tutorial/tree/camp4/docs/L0/linux 出现报错,是ssh配置原因 [23:40:18.788] Log Level: 2 [23:40:18.807] SSH Resolver called for “ssh-remotessh.intern-ai.org.cn”, attempt 1 [23:40:18.8…

使用 PyTorch-BigGraph 构建和部署大规模图嵌入的完整教程

当涉及到图数据时,复杂性是不可避免的。无论是社交网络中的庞大互联关系、像 Freebase 这样的知识图谱,还是推荐引擎中海量的数据量,处理如此规模的图数据都充满挑战。 尤其是当目标是生成能够准确捕捉这些关系本质的嵌入表示时,…

测试标题1111

前言 本文是该专栏的第68篇,后面会持续分享python爬虫干货知识,记得关注。 在本专栏之前,笔者有详细介绍京东滑块验证码的解决方法,感兴趣的同学,可以直接翻阅文章《Python如何解决“京东滑块验证码”(5)》进行查看。…

JDK8-17新特性

1.Java8新特性-Lambda表达式 2.1关于Java8新特性简介 Java 8是Java编程语言的一个重大版本更新,于2014年3月发布。它引入了许多新特性和改进,使得Java编程更加方便和高效。 下面是Java 8的主要新特性: Lambda表达式:Lambda表达式…

如何确保Python爬虫程序的稳定性和安全性?

在当今数字化时代,Python爬虫被广泛应用于数据采集和信息抓取。然而,确保爬虫程序的稳定性和安全性是开发过程中的重要考虑因素。本文将探讨如何通过技术手段和最佳实践来提高Python爬虫的稳定性和安全性,并提供代码示例。 稳定性保障 1. 异…

Axure二级菜单下拉交互实例

1.使用boxlabe进行基础布局 2.设置鼠标悬浮和选中状态 3.转换为动态面板 选中所有二级菜单,进行按钮组转换 选中所有二级菜单,进行动态面板转换 4.给用户管理增加显示/隐藏事件 1)选择toggle代表上拉和下拉切换加载 2)勾选Bring to Front,并选择Push/Pull Widgets代表收缩时…

基于智能推荐的图书电商系统的设计与实现

💗博主介绍💗:✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示:文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

JavaScript实现Promise

第一步:编写constructor构造方法 const PENDING pending; const FULFILLED fulfilled; const REJECTED rejected;class MyPromise {#state PENDING;#result undefined;constructor(executor) {const resolve (data) > {this.#changeState(FULFILLED, data…

物理 + 人工智能 = 2024年诺贝尔物理学奖

💓 博客主页:倔强的石头的CSDN主页 📝Gitee主页:倔强的石头的gitee主页 ⏩ 文章专栏:《热点时事》 期待您的关注 目录 引言 一、机器学习与神经网络的发展前景 二、机器学习和神经网络的研究与传统物理学的关系 结…

C++:异常

1. 异常的概念 C语言主要通过错误码的方式处理错误,错误码本质上就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。异常时抛出一个对象,这个对象可以涵盖更全面的信息。 异常处理机制允许程序中独立开发的…

南京邮电大学算法设计-二叉树先序遍历算法动态演示

二叉树先序遍历算法动态演示 一、课题内容和要求 (1)实验目的: 本实验通过手动输入二叉树结点信息,构建相应的二叉树,并通过图形化界面动态演示先序遍历算法的过程。通过本次实验,我可以深入理解二叉树的数据结构、先序遍历算法…

【开源免费】基于Vue和SpringBoot的在线考试系统(附论文)

本文项目编号 T 624 ,文末自助获取源码 \color{red}{T624,文末自助获取源码} T624,文末自助获取源码 网络的广泛应用给生活带来了十分的便利。所以把在线考试管理与现在网络相结合,利用java技术建设在线考试系统,实现…

高阶C语言之六:程序环境和预处理

本文介绍程序的环境,在Linux下对编译链接理解,较为简短,着重在于编译的步骤。 C的环境 在ANSI C(标准C语言)的任何一种实现中,存在两个不同的环境。 翻译环境:在这个环境中,源代码…

HarmonyOs鸿蒙开发实战(10)=>状态管理-对象数组的属性数据变更刷新UI,基于@Observed 和@ObjectLink装饰器

1.条件:基于HarmonyOs5.0.0版本. 2.功能要求:横向列表中每个景点的名称(eg: 第二项 “灵隐寺” ), 在通过天气接口拿到对应天气后,拼接到名称后面 > 变成(“灵隐寺” 天气)) 3.老规矩先看…