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

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

前言

最近工作中有这么一个需求,我们系统出单后,需要同步数据到合作方,合作方对数据接收并解析反馈结果文件给我们,根据结果文件状态判断合作方系统是否解析成功,对于失败的单子,需要邮件通知相关负责人。所以这里我们需要用到邮件发送工具,在使用时如果每次都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();
        }
    }
}

f54a834c9835c3bf84ecd92fee1e553b_1352c35ed4bb498db947e6cf29c044f8.png

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

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

代码示例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();
        }
    }
}

85c681df332577ba849e540c5b69c0a6_cf98bc4810e4459b9d217117890050df.png

这种方式显然是有问题的,在if (expose==null)判断的时候:


线程1符合条件进入了,但是还没有执行synchronized代码,未抢占锁

线程2获得了CPU的使用权,此时expose还是为null,所以也进入了该逻辑

线程2进行synchronized代码块实例化了实例,并释放了锁

线程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://ucc.alicdn.com/images/user-upload-01/5a5dd94476af43eaa50285c32c38eac7.png = 800x)我们可以看到结果符合我们的预期,但是大家不要高兴得太早,在极端情况下这种也会存在问题。


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


当new之后,会在堆内存中开启一块内存空间,此时内存空间为Null

调用构造器后,会往开启的内存空间填充数据

将内存空间地址引用赋值给栈中的变量,也就是变量名称

由于此步骤中步骤2和步骤3是没有关联性的,所以在CPU执行指令的时候可能先执行3再执行2,如果是这种情况的话,那么由于栈有了引用了,但是还未填充数据,导致此时该实例的属性值为默认值(int为0,引用类型为null),那么其它线程拿到该单例的时候,对属性进行读取或者修改就会出现问题。

步骤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关键字修改,可以避免指令重排的情况;

总结

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



目录
相关文章
|
SQL 分布式计算 资源调度
线上 hive on spark 作业执行超时问题排查案例分享
线上 hive on spark 作业执行超时问题排查案例分享
|
12月前
|
消息中间件 存储 缓存
招行面试:如何让系统抗住双十一 预约抢购活动?10Wqps级抢购, 做过吗?
本文由40岁老架构师尼恩撰写,针对一线互联网企业如得物、阿里、滴滴等的面试题进行深度解析。文章聚焦于如何设计系统以应对大促活动中的预约抢购场景,涵盖从预告到支付的完整流程。尼恩通过系统化、体系化的梳理,帮助读者提升技术实力,轻松应对高并发挑战,并提供了详细的架构设计和解决方案。文中还分享了《尼恩Java面试宝典》等资源,助力求职者在面试中脱颖而出,实现“offer直提”。更多内容及PDF资料,请关注公众号【技术自由圈】获取。
|
10月前
|
图形学 UED
unity Tab键实现切换输入框功能
该脚本用于简化输入框之间的Tab键切换操作。只需将脚本挂载在InputField上,无需其他设置。脚本通过监听Tab键和Shift键组合,自动选择下一个或上一个可交互的InputField,提升用户体验。
|
Ubuntu Linux Shell
C++ 之 perf+火焰图分析与调试
【10月更文挑战第8天】在遇到一些内存异常的时候,经常这部分的代码是很难去进行分析的,最近了解到Perf这个神器,这里也展开介绍一下如何使用Perf以及如何去画火焰图。
273 1
|
存储 编解码 数据挖掘
一篇文章掌握大厂成本优化框架
一篇文章掌握大厂成本优化框架
|
消息中间件 运维 监控
【Kafka】Kafka生产过程中何时会发生QueueFullExpection以及如何处理
【4月更文挑战第11天】【Kafka】Kafka生产过程中何时会发生QueueFullExpection以及如何处理
|
机器学习/深度学习 人工智能 监控
人工智能在金融风险管理中的应用
人工智能在金融风险管理中的应用已经取得了显著的进展,并在提高风险管理效率和准确性方面发挥了重要作用。通过信用评估、欺诈检测、投资组合管理等应用,人工智能为金融行业带来了新的机遇和挑战。然而,我们也要认识到人工智能在风险管理中可能面临的隐私、解释性和偏差等问题。未来,随着技术的发展,人工智能将在金融领域持续发挥重要作用,为金融行业创造更加安全和稳健的环境。
1475 1
|
Java 程序员 Spring
如何理解AOP中的连接点(Joinpoint)、切点(Pointcut)、增强(Advice)、引介(Introduction)、织入(Weaving)、切面(Aspect)这些概念?
a. 连接点(Joinpoint):程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。
2675 0
|
Java Spring .NET
AspectJ切入点语法详解
Spring AOP支持的AspectJ切入点指示符 切入点指示符用来指示切入点表达式目的,,在Spring AOP中目前只有执行方法这一个连接点,Spring AOP支持的AspectJ切入点指示符如下: execution:用于匹配方法执行的连...
2084 0