工作中单例模式用法及其使用场景?

news2025/7/7 18:09:48

前言

最近工作中有这么一个需求,我们系统出单后,需要同步数据到合作方,合作方对数据接收并解析反馈结果文件给我们,根据结果文件状态判断合作方系统是否解析成功,对于失败的单子,需要邮件通知相关负责人。所以这里我们需要用到邮件发送工具,在使用时如果每次都new 邮件工具那么就比较耗费堆内存空间,所以这里我们使用单例模式,在整个系统运行使用过程中,只需要new 一次即可。

单例模式有几种用法,需要根据具体的业务场景来制定,如:饿汉模式、懒汉模式、线程安全版的懒汉模式,下面来看下具体的使用方法。

正文

饿汉单例模式

代码示例

public class MyEmail {
    private static final EmailService expose=new EmailService();
    
    public static EmailService getInstance(){
        return expose;
    }
}

使用场景

什么时候使用它呢?我个人理解,如果服务器内存空间够大,可以使用这种方式,因为服务在启动过程中会有大量的类被实例化,如果系统中很多采用这种饿汉写法,如果堆栈空间不够的话,可能会导致内存溢出。

优缺点

优点

饿汉版的单例模式可以保证线程安全,因为类加载到内存后就会进行实例化,JVM可以保证其线程安全,这个版本比较简单实用。

缺点

饿汉版版单例模式唯一的缺点就是不管系统是否有使用到,都会进行实例化。

懒汉单例模式

代码示例

public class MyEmail {

    private static  EmailService expose=null;
    
    public static EmailService getInstance(){
        if (expose==null){
            expose=new EmailService();
        }
        return expose;
    }
    
}

使用场景

我个人理解系统启动实例化时,需要大量的堆栈空间,而非急切用到的类实例,我们可以将其放到使用时在进行实例化,这样可以提高系统的稳定性;

优缺点

优点

延迟加载可以提高项目启动时速度及稳定性

缺点

虽然达到了按需初始化的目的,但却带来线程不安全的问题,下面看下这个测试用例:

public class MyEmail {

    private static  EmailService expose=null;
    
    public static EmailService getInstance(){
        if (expose==null){
            try {
            	//提高CPU让出当前线程执行其它线程的概率
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            expose=new EmailService();
        }
        return expose;
    }
    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                    System.out.println(MyEmail.getInstance().hashCode())
            ).start();
        }
    }
}

在这里插入图片描述

由上面的测试用例,可以看到多线程的情况下存在实例被实例化多次的情况。

懒汉单例模式(线程安全版)

代码示例1

public class MyEmail2 {

    private static  EmailService expose=null;
    
    public static synchronized EmailService getInstance(){
        if (expose==null){

            expose=new EmailService();
        }
        return expose;
    }

}

直接在方法上加synchronized,这种方式比较简单粗暴。但是效率比较低,因为锁的是整个方法,如果该方法体里面的代码比较多或者说执行时间比较长,那其它线程只能这样干等着,这会导致CPU的效率低下。

代码示例2

代码示例1中直接加锁的方式效率低下,那么我们可以采用细化锁来提高效率。

步骤1:

public class MyEmail3 {

    private static  EmailService expose=null;
    
    public static  EmailService getInstance(){
        if (expose==null){
            synchronized (MyEmail3.class){
                expose=new EmailService();
            }
        }
        return expose;
    }

}

大家觉得这种写法是否可以实现线程安全呢?口说无凭,我们写个测试用例来校验一下;

public class MyEmail3 {

    private static  EmailService expose=null;
    
    public static  EmailService getInstance(){
        if (expose==null){
            try {
            	//提高CPU让出当前线程执行其它线程的概率
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (MyEmail3.class){
                expose=new EmailService();
            }

        }
        return expose;
    }
    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                    System.out.println(MyEmail3.getInstance().hashCode())
            ).start();
        }
    }
}

在这里插入图片描述这种方式显然是有问题的,在if (expose==null)判断的时候:

  1. 线程1符合条件进入了,但是还没有执行synchronized代码,未抢占锁
  2. 线程2获得了CPU的使用权,此时expose还是为null,所以也进入了该逻辑
  3. 线程2进行synchronized代码块实例化了实例,并释放了锁
  4. 线程1获得锁,进入synchronized代码块进行实例化

基于以上几点,这种方式会存在多次创建实例的情况

步骤2:

基于步骤1的缺点,我们在synchronized中再加上一层If判断。

    public static  EmailService getInstance(){
        if (expose==null){
            synchronized (MyEmail3.class){
                if (expose==null){
                    expose=new EmailService();
                }
            }
        }
        return expose;
    }

我们来运行测试用例:

public class MyEmail3 {

    private static  EmailService expose=null;
    
    public static  EmailService getInstance(){
        if (expose==null){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (MyEmail3.class){
                if (expose==null){
                    expose=new EmailService();
                }
            }

        }
        return expose;
    }
    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                    System.out.println(MyEmail3.getInstance().hashCode())
            ).start();
        }
    }
}

![在这里插入图片描述](https://img-blog.csdnimg.cn/5a5dd94476af43eaa50285c32c38eac7.png = 800x)我们可以看到结果符合我们的预期,但是大家不要高兴得太早,在极端情况下这种也会存在问题。

我们需要了解下创建实例的过程(个人理解,要求严谨可自查资料):

  1. 当new之后,会在堆内存中开启一块内存空间,此时内存空间为Null
  2. 调用构造器后,会往开启的内存空间填充数据
  3. 将内存空间地址引用赋值给栈中的变量,也就是变量名称

由于此步骤中步骤2和步骤3是没有关联性的,所以在CPU执行指令的时候可能先执行3再执行2,如果是这种情况的话,那么由于栈有了引用了,但是还未填充数据,导致此时变量为null,那么在第二个If判断就会失效了。

步骤3:

public class MyEmail3 {

    private static volatile   EmailService expose=null;
    
    public static  EmailService getInstance(){
        if (expose==null){

            synchronized (MyEmail3.class){
                if (expose==null){
                    expose=new EmailService();
                }
            }
        }
        return expose;
    }

}

使用volatile关键字修改,可以避免指令重排的情况;

总结

工作中具体使用哪种单例模式,需要根据业务场景综合考虑,没有最好只有适合。

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

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

相关文章

微服务实战02-EurekaServer注册中心

EurekaServer &#xff0c;它扮演的角色是注册中心&#xff0c;用于注册各种微服务&#xff0c;以便于其他微服务找到和访问。 1、Eureka是什么 Eureka是Netflix的一个子模块&#xff0c;也是核心模块之一。Eureka是一个基于REST的服务&#xff0c;用于定位服务&#xff0c;以…

AUTOSAR为啥要开发新的社区商业模式?

总目录链接>> AutoSAR入门和实战系列总目录 文章目录1 自适应平台架构中的集群更新1.1 ara::diag 服务&#xff08;诊断&#xff09;更新1.2 信号到服务映射和自动驾驶接口让我们讨论一下信号到服务映射服务:Automated Driving Interface:2 车载应用商店概念本文介绍Re…

【RabbitMQ笔记09】消息队列RabbitMQ之常见方法的使用

这篇文章&#xff0c;主要介绍消息队列RabbitMQ之常见方法的使用。 目录 一、消息队列常见方法 1.1、连接工厂ConnectionFactory 1.2、连接Connection 1.3、通道Channel 1.4、交换机相关方法 &#xff08;1&#xff09;exchangeDeclare()声明交换机 1.5、队列相关方法 …

算法训练营 day58 动态规划 判断子序列 不同的子序列

算法训练营 day58 动态规划 判断子序列 不同的子序列 判断子序列 392. 判断子序列 - 力扣&#xff08;LeetCode&#xff09; 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而…

03、SVN 建立版本库

SVN 建立版本库1 版本库2 版本库的建立步骤2.1 创建版本库的根目录2.2 创建子目录2.3 通过命令创建版本库2.4 生成目的介绍1 版本库 Subversion 是将文件数据信息保存到版本库中进行管理的Subversion 允许用户对版本库目录进行定制 2 版本库的建立步骤 2.1 创建版本库的根目…

引用数据类型和基本数据类型

1.基本数据类型&#xff1a; byte&#xff1a;字节类型&#xff0c;Java中最小的数据类型&#xff0c;1个字节&#xff0c;取值范围-128~127&#xff0c;默认值0 char&#xff1a;字符型&#xff0c;用于存储单个字符&#xff0c;2个字节&#xff0c;取值范围0~65535&#xf…

Redis使用,AOF、RDB

前言 如果有人问你&#xff1a;"你会把 Redis 用在什么业务场景下&#xff1f;" 我想你大概率会说&#xff1a;"我会把它当作缓存使用&#xff0c;因为它把后端数据库中的数据存储在内存中&#xff0c;然后直接从内存中读取数据&#xff0c;响应速度会非常快。…

“速通“ 老生常谈的HashMap [实现原理源码解读]

&#x1f473;我亲爱的各位大佬们好&#x1f618;&#x1f618;&#x1f618; ♨️本篇文章记录的为 HashMap 实现原理&&源码解读 相关内容&#xff0c;适合在学Java的小白,帮助新手快速上手,也适合复习中&#xff0c;面试中的大佬&#x1f649;&#x1f649;&#x1f…

RocketMQ动态增加NameServer

前言 通过HTTP服务来设置NameServer地址&#xff0c;是唯一支持动态增加NameServer的方式&#xff0c;无需重启其它组件 网上搜了下没看到有兄弟们演示这块&#xff0c;所以刚才自己试了试&#xff0c;做个笔记 本文有详细的演示过程 都知道NameServer有4种配置方式&#xff0…

simulink入门指南

系列文章目录 文章目录系列文章目录常用操作技巧项目练习 - 动态系统二阶系统建模比例积分控制燕子俯冲系统建模滤波位操作总结常用操作技巧 画面缩放 空格键 鼠标左键: 拖拽空格键: fit屏幕 信号操作 双击signal可添加标签, 鼠标右键拖拽可给信号添加分支同一分支上的sig…

【并发基础】线程,进程,协程的详细解释

目录 一、什么是进程和线程 1.1 进程是什么呢&#xff1f; 1.2 线程又是什么呢&#xff1f; 1.3 线程和进程之间的关系 操作系统、进程、线程之间的关系图&#xff1a; 进程与线程的模型图&#xff1a; 下面来思考这样一个问题&#xff1a;为什么程序计数器、虚拟机栈和本地方法…

QNX7.1 交叉编译开源库

1.下载QNX7.1 SDK并解压 ITL:~/work/tiqnx710$ ls -l 总用量 16 drwxrwxr-x 4 xxx4096 1月 28 13:38 host -rwxrwxr-x 1 xxx 972 1月 28 13:38 qnxsdp-env.bat -rwxrwxr-x 1 xxx 1676 1月 28 13:38 qnxsdp-env.sh drwxrwxr-x 3 xxx 4096 1月 28 13:38 target xxxITL:~/work/ti…

NetApp SnapCenter 备份管理 ——借助应用程序一致的数据备份管理,简化混合云操作

NetApp SnapCenter 简单、可扩展、赋权&#xff1a;跨 Data Fabric 的企业级数据保护和克隆管理 主要优势 • 利用与应用程序集成的工作流和预定义策略简化备份、恢复和克隆管理。 • 借助基于存储的数据管理功能提高性能和可用性&#xff0c;并缩短测试和开发用时。 • 提供基…

干货 | 浅谈机器人强化学习--从仿真到真机迁移

“对于机器人的运动控制&#xff0c;强化学习是广受关注的方法。本期技术干货&#xff0c;我们邀请到了小米工程师——刘天林&#xff0c;为大家介绍机器人&#xff08;以足式机器人为主&#xff09;强化学习中的sim-to-real问题及一些主流方法。”一、前言设计并制造可以灵活运…

【靶机】vulnhub靶机cybox1

Vulnhub靶机Cybox下载地址 Cybox: 1.1 ~ VulnHub 信息搜集 首先进行靶机ip的发现。 sudo arp-scan -l 接着使用nmap扫描开放的端口&#xff0c;这里使用扫描全部端口和详细信息太慢了&#xff0c;分开检测 命令&#xff1a;nmap -p 21,25,80,110,143,443 -sV -A 192.168.174…

CentOS救援模式(Rescue Mode)及紧急模式(Emergency Mode)

当CentOS操作系统崩溃&#xff0c;无法正常启动时&#xff0c;可以通过救援模式或者紧急模式进行系统登录。启动CentOS, 当出现下面界面时&#xff0c;按e进入编辑界面。在编辑界面里&#xff0c;加入参数&#xff1a;systemd.unitrescue.target &#xff0c;然后Ctrl-X启动进入…

Linux服务器上传文件到阿里云oss对象存储的两种方法ossutil、curl

ossutil支持在Windows、Linux、macOS等系统中运行&#xff0c;您可以根据实际环境下载和安装合适的版本。安装过程中&#xff0c;需要使用解压工具&#xff08;unzip、7z&#xff09;解压软件包&#xff0c;请提前安装其中的一个解压工具。yum -y install unzipLinux系统一键安…

正点原子第一期

ZYNQ是一个fpga用来硬件编程&#xff0c;外加一个软件编程 FPGA是可通过编程来修改其逻辑功能的数字集成电路 第三篇语法篇 第七章 verilog HDL语法 Verilog的简介 可编程逻辑电路&#xff1a;允许用户自行修改内部连接的集成电路&#xff0c;其内部的电路结构可以通过编程数…

ADAS-CIS相机关键参数综述

引言 “ CIS传感器关键参数是工程师选型参考的依据&#xff0c;抛开镜头&#xff0c;本文介绍CIS可见光传感器关键参数之CRA、QE、CFA、Shutter、SNR等。” 关键参数 CRA CFA(Chief Ray Angle)主光角&#xff0c;在上一篇可见光相机的介绍中我们介绍了Sensor的叠层结构&am…

leecode+剑指offer

1.算法入门14天 1.704二分查找&#xff1a; 题目描述&#xff1a;给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 思路…