【单例模式】保证线程安全实现单例模式

📄前言:本文是对经典设计模式之一——单例模式的介绍并讨论单例模式的具体实现方法。


文章目录

  • 一. 什么是单例模式
  • 二. 实现单例模式
    • 1. 饿汉式
    • 2. 懒汉式
      • 2.1 懒汉式实现单例模式的优化(一)
      • 2.2 懒汉式实现单例模式的优化(二)
    • 3. 饿汉式和懒汉式的对比

一. 什么是单例模式

以下单例模式的概念:

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

“说人话”版本:单例模式是指某个类在程序运行过程中当且仅当会被实例出一个对象的设计模式。

为什么要使用单例模式?
在一个程序中,若多个地方都需要用到一个类的某些方法且这些方法实现的功能完全一样时,如果实例化出多个对象,会造成内存空间的浪费,占用系统资源。
例如:当我们在Java程序中需要进行数据库操作时,首先需要获得一个数据源(DataSource)来确定数据库的唯一网络资源位置,要进行数据库操作只需通过同一个数据源建立连接,在这个场景下 数据源对象 只需要一个,从而避免了系统资源的浪费。
在这里插入图片描述


二. 实现单例模式

实现单例模式有以下两个关键点:

  1. 单例模式下类只能有一个实例化的对象,因此该类不能通过构造方法任意实例化,其构造方法应该私有化
  2. 想获得该类的实例对象,可以通过类的静态方法来获取。

单例模式按实现的方式可以分为以下两种:

  • 饿汉式:在类加载时就创建出对象
  • 懒汉式:在获取对象实例时才创建对象(使用时)

1. 饿汉式

饿汉即形容一个人在肚子饥饿时便一次性把自己吃撑,后续便不再进食。饿汉式实现单例模式即使一个类在程序的类加载的阶段便创建出对象,后续程序中想使用该对象就可以直接获取。(这里可以简单理解为程序启动后类就会被实例化)

饿汉式实现单例模式可将代码实现分为以下几步:
1.定义一个由私有的、不可修改的、静态的类属性并进行实例化。
2.将构造方法私有化
3.定义一个方法,使类属性可以被获取。

具体的代码实现如下:

class SingleTon1 {//饿汉模式,即在类加载时就实例化出对象private final static SingleTon1 instance = new SingleTon1();// 使构造方法私有化,保证类的实例只能被创建一个private SingleTon1() {}// 通过静态方法获取类对象public static SingleTon1 getInstance() {return instance;}
}

2. 懒汉式

懒汉即形容一个人在饥饿时才选择进食且不一次性吃饱,等待后续饥饿便再次进食。懒汉式实现单例模式即在第一次调用方法获取类的实例对象时才进行创建,后续程序中想使用该对象就可以直接获取。

懒汉式实现单例模式可将代码实现分为以下几步:
1.声明一个私有的、静态的类属性。
2.将构造方法私有化
3.定义一个方法,使类属性可以被获取;当该方法被调用时,判断类属性的值并决定是否进行类的实例化。

具体的代码实现如下:

class SingleTon2 {private static SingleTon2 instance;// 使构造方法私有化,保证类的实例只能被创建一个private SingleTon2() {}// 判断是否存在实例对象,没有则创建对象并放回public static SingleTon2 getInstance() {if(instance == null) {instance = new SingleTon2();}return instance;}
}

在饿汉式创建单例对象的基础上,我们只做出了微小的改动便实现了懒汉式单例模式。那么上面的代码是否就是正确的呢?
答案是:不完全正确。因为上述代码在单线程环境中运行没有问题,但在多线程的环境下就可能出现“错误”,导致理想中的单例模式被打破

下面模拟在多线程环境下使用上述懒汉模式代码获取实例对象,程序中用一个静态成员变量 count 来记录类被实例化的次数

class SingleTon3 {public static int count;private static SingleTon3 instance;// 使构造方法私有化,保证类的实例只能被创建一个private SingleTon3() {}// 判断是否存在实例对象,没有则创建对象并返回public static SingleTon3 getInstance() {if(instance == null) {instance = new SingleTon3();count++;}return instance;}
}public class Demo25 {public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[10];for (int i = 0; i < 10; i++) {threads[i] = new Thread(() -> {SingleTon3 instance = SingleTon3.getInstance();});threads[i].start();}// 等待所有线程执行完毕for (int i = 0; i < 10; i++) {threads[i].join();}// 获取类的实例化次数System.out.println(SingleTon3.count);}}

代码的可能结果如下:(因为多线程的抢占式执行,每次的执行结果可能并不相同)
在这里插入图片描述

2.1 懒汉式实现单例模式的优化(一)

为什么会出现上述现象呢?饿汉式实现单例模式是否也会出现这种现象?
最根本的原因是:在多线程环境下对一个共享的数据进行了修改操作。当 instance 还未被实例化时,因为线程的抢占式执行,导致出现了多个线程同时执行到了 if 条件的判断,这些线程都认为 instance 未被实例化,因此各自初始化了一个类对象,造成了单例模式被打破。(执行情况如下图)
通过以上分析,我们很容易知道通过饿汉式的实现方式并不会出现“单例模式被破坏”的现象,因为他的类属性在类加载时便已初始化完毕,且获取该属性时并不涉及修改操作,因此饿汉式保证了在单线程或多线程下的绝对安全。
在这里插入图片描述

如何防止这种情况的发生呢?
在多线程的场景中,毫无疑问使用 synchronized 对修改操作进行加锁是其中的一个解决办法。

如何进行有效加锁?
由上图可以知道,导致出现类被多次实例的原因在于 if 语句的判断出现错误,因此想要进行有效加锁,需要每个未获取锁的线程在进行 if 语句的判断前进入阻塞状态,等待第一个获取锁的线程示例出一个类对象时,其他的线程才可进行 类属性是否为空的判断。(代码如下)

class SingleTon2 {private static SingleTon2 instance;private SingleTon2() {}public static SingleTon2 getInstance() {// 对 if 条件判断语句进行加锁操作synchronized (SingleTon2.class) {if(instance == null) {instance = new SingleTon2();}}return instance;}
}

上述代码实际上已经能够保证多线程下的安全问题,可初始化了类对象后,后续对 if条件的判断 其实已经失去了加锁的必要性,因为类属性已被实例化,多余的加锁操作会增加系统的开销,增加程序的运行时间。
因此,我们需要对是否进行加锁再进行一次判断。(修改代码如下)

private static volatile SingleTon2 instance;private static SingleTon2 instance;private SingleTon2() {}public static SingleTon2 getInstance() {// 第一次对象实例化后,后续并不涉及 修改操作,无需重复加锁判断if(instance == null) {// 在多线程 并发执行下,防止 创建多个实例synchronized (SingleTon2.class) {if(instance == null) {instance = new SingleTon2();}}}return instance;}

2.2 懒汉式实现单例模式的优化(二)

上述已经完美解决了类属性被多次实例化的线程安全问题,但其实还存在另一个潜在的安全问题:因 new() 操作触发的指令重排序造成的多线程安全问题。
什么是指令重排序?
JVM 在保证最终代码执行逻辑不变的情况下,对某一段指令的执行顺序做出了调整,从而提高了程序的执行效率。

new()操作实际会被拆分为以下3步:
1.申请一块内存空间
2.在内存空间上利用构造方法构造对象
3.把对象在内存中的地址赋值给 instance 引用

当第一个线程调用静态方法获取类属性时,因 new()操作触发了指令重排序,先执行了第1、3步操作,此时 instance引用不为空,但还未对对象的属性和方法进行初始化。若此时后续的线程经过 if 判断后得到了 instance 引用,并使用了这个还没初始化的非法对象的属性或方法时,就可能出现不可预期的错误。

因此,instance 属性需要用 volatile 关键字来禁止指令重排序。(代码如下)

class SingleTon2 {// 禁止指令重排序, 防止未实例完成的对象里的属性 被非法使用private static volatile SingleTon2 instance;private SingleTon2() {}public static SingleTon2 getInstance() {// 第一次对象实例化后,后续并不涉及 修改操作,无需重复加锁判断if(instance == null) {// 在多线程 并发执行下,防止 创建多个实例synchronized (SingleTon2.class) {if(instance == null) {instance = new SingleTon2();}}}return instance;}
}

3. 饿汉式和懒汉式的对比

  1. 饿汉式在程序启动后的类加载阶段就创建出类对象,能够直接使用实例对象;懒汉式在使用时才加载。
  2. 饿汉式不存在多线程安全问题;懒汉式可能存在多线程安全问题,需要对代码实现进行优化。
  3. 对内存要求不高的场景中可以直接使用饿汉式写法;对内存要求高的场景下,可以使用懒汉式写法,在需要使用时才创建对象。

以上就是本篇文章的全部内容了,如果这篇文章对你有些许帮助,你的点赞、收藏和评论就是对我最大的支持。
另外,文章可能存在许多不足之处,也希望你可以给我一点小小的建议,我会努力检查并改进。

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

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

相关文章

仿真机器人-深度学习CV和激光雷达感知(项目2)day04【简单例程】

文章目录 前言简单例程运行小海龟仿真启动节点查看计算图发布 Topic调用 Serviece 用 Python 发布和接收 Topic创建工作空间创建功能包&#xff0c;编译编写 Topic Publisher 节点编写 Topic Subscriber 节点运行节点 自定义消息类型用 Python 注册和调用 Serviece新建功能包在…

【若依】关于对象查询list返回,进行业务处理以后的分页问题

1、查询对象Jglkq返回 list&#xff0c;对 list 进行业务处理后返回&#xff0c;但分页出现问题。 /*** 嫁功率考勤查询*/RequiresPermissions("hr:kq:list")PostMapping("/list")ResponseBodypublic TableDataInfo list(Jglkq jglkq) throws ParseExcepti…

Spring基于dynamic-datasource实现MySQL多数据源

目录 多数据源实现 引入依赖 yml配置文件 业务代码 案例演示 多数据源实现 引入依赖 <dependency><groupId>com.baomidou</groupId><artifactId>dynamicdatasourcespringbootstarter</artifactId><version>3.5.0</version> &…

[Python] glob内置模块介绍和使用场景(案例)

Unix glob是一种用于匹配文件路径的模式&#xff0c;它可以帮助我们快速地找到符合特定规则的文件。在本文中&#xff0c;我们将介绍glob的基本概念、使用方法以及一些实际应用案例。 glob介绍 Glob(Global Match)是Unix和类Unix系统中的一种文件名扩展功能&#xff0c;它可以…

【python文件】生成的csv文件没两行数据之间有一个空行

问题描述 用python代码将数据写入csv文件&#xff0c;但生成的csv文件没两行数据之间有一个空行&#xff0c;如下图所示&#xff1a; 解决办法 在open函数中添加newline&#xff0c;如以下代码所示&#xff0c;即可解决这一问题。 with open(r"C:\Users\xxx\Desktop\DR…

未来已来:AI引领智能时代的多领域巨变

大家好&#xff0c;今天我们将深入探讨人工智能如何彻底改变我们的生活方式&#xff0c;领略未来的无限可能性。 1. 医疗革新&#xff1a;AI担任超级医生 医疗领域是AI最引人注目的战场之一。智能医学影像诊断系统&#xff0c;不仅能够精准识别病变&#xff0c;还能辅助医生提…

使用人工智能助手 Github Copilot 进行编程 02

本章涵盖了 在您的系统上设置 Python、VS Code 和 Copilot引⼊ Copilot 设计流程Copilot 的价值在于基本的数据处理任务本章将帮助您在自己的计算机上开始使用 Copilot,并熟悉与其的交互方式。在设置好Copilot 后,我们将要求您尽可能跟随我们的示例进行操作。实践是最好的学习…

从零开始训练 YOLOv8最新8.1版本教程说明(包含Mac、Windows、Linux端 )同之前的项目版本代码有区别

从零开始训练 YOLOv8 - 最新8.1版本教程说明 本文适用Windows/Linux/Mac:从零开始使用Windows/Linux/Mac训练 YOLOv8 算法项目 《芒果 YOLOv8 目标检测算法 改进》 适用于芒果专栏改进 YOLOv8 算法 文章目录 官方 YOLOv8 算法介绍改进网络代码汇总第一步 配置环境1.1 系列配…

动态IP代理与静态IP代理:详细区别与比较全析

动态代理IP和静态代理IP在跨境业务中具有非常广泛的实用性&#xff0c;但仍然有非常多小白选手并不清楚什么场景适合用哪一类IP&#xff0c;哪一中代理IP类型更适合你&#xff1f;其实他们各有其优点和缺点&#xff0c;为了使您的网络营销、社媒推广、跨境电商运营、网络抓取尽…

【排序算法】C语言实现归并排序,包括递归和迭代两个版本

文章目录 &#x1f680;前言&#x1f680;归并排序介绍及其思想&#x1f680;递归实现&#x1f680;迭代实现 &#x1f680;前言 大家好啊&#xff01;阿辉接着更新排序算法&#xff0c;今天要讲的是归并排序&#xff0c;这里阿辉将讲到归并排序的递归实现和迭代实现&#xff…

DA14531平台secondary_bootloade工程修改笔记

DA14531平台secondary_bootloade工程修改笔记 1.支持在线仿真 初始时加入syscntl_load_debugger_cfg(); 表示可以重复Jlink连接调试仿真 2.支持串口烧录,和支持单线线写 utilities\secondary_bootloader\includes\bootloader.h /************** 2-wire UART support ******…

【机器学习300问】17、什么是欠拟合和过拟合?怎么解决欠拟合与过拟合?

一个问题出现了&#xff0c;我们首先要描述这个问题&#xff0c;然后分析问题出现的原因&#xff0c;找到原因后提出解决方案。废话不多说&#xff0c;直接上定义&#xff0c;然后通过回归和分类任务的例子来做解释。 一、什么是欠拟合和过拟合&#xff1f; &#xff08;1&am…