<设计模式>单例模式懒汉和饿汉

目录

一、单例模式概述

二、懒汉模式和饿汉模式

1.饿汉模式

1.1代码实现

1.2实现细节

1.3模式优劣

2.懒汉模式

2.1代码实现

2.2实现细节

2.3模式优劣

三、多线程下的线程安全问题

1.懒汉和饿汉线程安全问题分析

1.1安全的饿汉模式

1.2不安全的懒汉模式

2.懒汉线程安全实现

2.1代码实现

2.2实现细节


一、单例模式概述

单例模式是一种创建型模式,它的目的是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。单例模式通常用于需要频繁创建和销毁同一对象的场景,通过单例模式可以减少系统性能开销,提高系统性能。

基础的单例模式分为两种,懒汉模式和饿汉模式。

例如对于相当大的对象(假设其管理10G的数据),使用一次创建一次或是过多创建该对象都会造成较大的系统性能开销,那我们能不能规定整个这个类只能创建出一个实例,即每次创建实例返回的都是同一个对象,这样不但避免了使用一次就创建一次的性能开销,也能避免创建多个对象对空间资源的浪费。

可以结合下例进一步理解:

 对于一个“自助调料区”对象,在火锅店中需要他的“客人”线程可以在任何时间任何地区,同时同地的前来操作“自助调料区”对象,获取需要的内容。

在这一场景下,多个“自助调料区”对象无疑是没有必要的,正是单例模式大显身手的地方,采用了单例模式的火锅店就像是下达了“禁止多设调料区,需要调料都到这一个调料区”的指令,避免了资源的浪费。

说了这么多,单例模式怎么使用,又是怎么实现的呢?

且看下文。

二、懒汉模式和饿汉模式

1.饿汉模式

饿汉模式是单例模式的一种简单实现,‘式如其名’,饿汉模式主打的就是一个饿死鬼投胎,即类加载阶段就已经把实例创建出来了,相当于程序已启动就有这个实例了,总之非常迫切的感觉。

在饿汉模式中,类加载的时候就已经实例化对象,即“饿汉”在类加载时就完成了初始化,因此可以保证只有一个实例存在。

1.1代码实现

虽然Java标准库没法直接规定类所能创建实例的数量,但我们依然可以通过一些方法限制实例的创建,间接达到只能创建一个实例的效果。

饿汉模式的代码具体实现如下:

class Singleton {//实例为static修饰的类属性,类加载阶段创建private static Singleton instance = new Singleton();//通过getInstance方法获取唯一实例public static Singleton getInstance() {return instance;}//私有构造方法,无法通过new创建该类实例private Singleton() {};
}
1.2实现细节

第二行代码“private static Singleton instance = new Singleton();”

instance变量是Singleton类创建的唯一实例,分别被‘private’关键字和‘static’关键字修饰。private保证了该变量为类所私有,外界无法直接访问和修改,只能通过下面的getInstance方法获取该实例。static表示类属性,即instance作为Singleton类的属性在类加载阶段就被创建出来,且具有唯一性。

第三~五行代码“getInstance()”

作为获取唯一实例的唯一方法存在,需要由public和static修饰,使外界可以通过类直接调用该方法。

第六行代码“private Singleton() {};”

private关键字使Singleton类的构造方法私有,这样外界就没法new该类了。

1.3模式优劣

这种方式实现简单,但会导致类加载时就创建对象,如果不需要使用该对象则会造成资源浪费。同时,由于实例化对象在类加载时完成,因此无法在运行时改变实例状态。

2.懒汉模式

不同于饿汉模式的急不可耐,懒汉模式采用的是摆烂策略,就像博主暑假在家一样,不喊我我就绝不出门,妥妥的宅男。

在懒汉模式中,类加载的时候不会实例化对象,而是在第一次调用getInstance方法时才实例化对象。

2.1代码实现
class SingletonLazy {private static SingletonLazy instance = null;//通过getInstance方法获取唯一实例public static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();}return instance;}//私有构造方法,无法通过new创建该类实例private SingletonLazy() {};
}
2.2实现细节

上述实现和饿汉模式的实现差别不大,只不过并没有在类加载阶段直接创建实例,而是在第一次调用getIntance方法时才创建出实例,即调用getIntance且instance=null未初始化时。

2.3模式优劣

只有在需要时才创建对象,节省了系统资源,只是实现上要比饿汉模式要更加复杂。由于在多线程环境下可能导致多个线程同时实例化对象,因此需要加锁来保证线程安全。

三、多线程下的线程安全问题

上述懒汉模式和饿汉模式的实现针对的是单线程情况的代码,多线程下代码实现是否会出现问题还需要具体分析。

1.懒汉和饿汉线程安全问题分析

1.1安全的饿汉模式

我们知道,产生线程安全的原因可能是内存可见性、锁竞争、优化策略和线程调度策略,及其它。具体产生问题的原因可能是多个线程对同一空间读写操作产生的。

再看饿汉模式的代码实现

很显然Singleton类中唯一的可调用方法getIntance只涉及到读操作,并不会产生线程安全问题。而由于不涉及到锁,更不会因为锁竞争陷入死锁。所以,饿汉模式是线程安全的。

1.2不安全的懒汉模式

再看懒汉模式,getIntance方法中不仅涉及了读操作同时也涉及了写操作,这就为线程安全问题的产生埋下了隐患。

由于线程调度的随机性,当两个线程在同一时间调用该方法时,错落的执行顺序可能导致if语句出现不可避免的错判,进而导致最终创建了两个SingletonLazy实例,如下图:

①T1线程执行完if语句,因为第一次调用getIntance方法,intance==null,所以T1线程接下来将要创建SingletonLazy实例,并将其赋值给intance。

②轮到T2线程执行,由于T1线程中尚未进行实例创建,此时仍旧是instance==null,所以if语句判断通过。接下来创建实例、赋值一气呵成,最后还将创建的Singleton对象返回。

③再次轮到T1线程,继续执行,创建了一个和T2线程不同的Singleton实例,引用赋给instance。最后,这个引用又被返回。

综上所述,在多线程情况下竟然出现了两个懒汉实例,这不符合单例模式下一个类只能创建一个实例的原则,很可能产生无法预估的错误,妥妥的bug代码。所以单线程下实现的懒汉模式不是线程安全的。

2.懒汉线程安全实现

上文分析懒汉模式代码线程不安全的原因是进程的随即调度问题,这一点我们可以通过引入锁来保证代码的原子性(一个整体)。同时,还要注意其他线程安全问题。

2.1代码实现
class SingletonLazy {//锁对象private static Object lock = new Object();//唯一实例,新增的volatile关键字是为了禁止指令重排序导致bugprivate static volatile SingletonLazy instance = null;public static SingletonLazy getInstance() {//当instance不为空时不进行加锁,提高代码效率if(instance == null) {//通过锁保证创建实例代码的原子性,不会因为线程的随即调度而产生多个实例synchronized(lock) {//多线程情况下可能由于锁竞争陷入阻塞,所以其他线程可能创建过实例了if(instance == null){//虽然new SingletonLazy可以分解为三个指令,// 但instance受volatile保护不会指令重排序instance = new SingletonLazy();}}}return instance;}private SingletonLazy() {};
}
2.2实现细节

通过锁保证代码块的原子性,进而克服系统随机调度的问题:

①T1线程执行。实例未创建,if判断通过。

②轮到T2线程。实例未创建,if判断通过。T2线程首先获取到锁资源,synchronized代码块执行完毕后才释放锁。if判断通过,实例创建成功后,锁释放。

③锁释放,T1线程解除阻塞,获取到锁资源。由于T2线程已经创建实例成功,if判断不通过,不创建新的实例,解锁,返回instance。

④轮到T2线程,返回instance的值。

上述过程返回的是同一个实例,成功克服系统随机调度的问题。

通过额外的if嵌套提高代码效率:

对于单线程代码来说,两个完全一样的if判断就是在脱裤子放屁,多此一举。但对于多线程代码来说,由于线程的随机调度,线程阻塞等问题,紧邻的两行代码执行时间相隔的可能是海枯石烂。

就我们的代码来说,外层if的作用是在实例已经创建的情况下,如果再调用本方法,只需经过该if语句就可以直接返回值,结束方法。像较于加锁解锁,if作为跳转语句效率相对非常之高,可以提高代码的运行效率。

而内层if则是判断线程阻塞时其他线程没有创建实例,确保只创建出一个实例。

通过volatile关键字保障创建操作不会因为代码优化(指令重排序)产生问题:

指令重排序是因为编译器会在保持“代码逻辑不发生变化”这一前提下对我们的代码进行优化,举个形象的例子:

1.去楼下超市买菜

2.回家

3.下楼倒垃圾

假如我们的代码执行逻辑为1->2->3,代码优化过后可能执行逻辑就变为1->3->2,两种执行逻辑效果相同,但效率却大大提高了。

而在多线程代码中,代码优化却可能会导致bug的出现。例如当线程频繁对同一个变量进行读值,在代码优化过后可能就不会再从主内存中读值,而是直接从线程的寄存器中读值,这时如果修改主内存的值,线程是感知不到的,从而导致线程安全问题的出现。

new Singleton()可以分解为以下三个指令:

1.申请一段内存空间

2.在这个内存上调用构造方法,创造出这个实例

3.把这个内存地址赋值给instance引用变量

指令重排序会在保持“代码逻辑不发生变化”这一前提下对我们的代码进行优化。对于逻辑而言,上述三个指令的顺序123和132都是没有区别的,因此执行顺序可能被优化成132。

程序执行是一条指令一条指令执行的,因此三条指令执行过程中线程可能就会被调度走了,如下:

①执行完毕后instance不为空,但引用指向的空间还未初始化,因为指令2还未执行。

②因为instance不为空,外层if判断未通过,返回未初始化空间的引用instance,方法结束。因为实列未初始化,而初始化的时间又无法确定(随机调度,T1要和其他线程竞争),这时候使用这个实例就可能产生问题。

③执行时间不确定,可能产生问题。

想要避免上述情况的出现,就必须保证指令的执行顺序保持不变为123,想达到这一效果可以使用volatile关键字修饰instance,利用其禁止指令重排序的特性。

博主是Java新人,每位同志的支持都会给博主莫大的动力,如果有任何疑问,或者发现了任何错误,都欢迎大家在评论区交流“ψ(`∇´)ψ

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

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

相关文章

用Python画一条祥龙,祝您新年龙腾万里

用Python画一条祥龙,祝您新年龙腾万里 龙年到了,祝大家新年龙行龘龘,龙腾万里! 从2021年开始,我每年都用Python画一幅当年生肖的图。 用Python标准库turtle画一头金牛,祝您新年牛气冲天! 用P…

SpringBoot项目打包成jar包供第三方使用实践

文章目录 1.使用者手动配置 basePackages1.1 第三方jar项目1.2 使用者项目1.2.1 使用者配置1.2.2 项目测试 2.使用者通过注解的方式引入2.1 第三方jar项目2.2 使用者项目2.2.1 使用者配置2.2.2 项目测试 3.SpringBoot Starter 方式3.1 第三方jar项目3.2 使用者项目3.2.1 使用者…

Unity3d C# 在WebGL平台加载并解析xml文件实现总结

前言 xml是可扩展标记语言,由一系列的元素、属性、值节点等构成的一个树形结构,除了可读性差一点,别的用于存储一些结构化的数据还是比较方便的。这个功能在Unity3d端的实现是比较方便快捷的: void GetXML1() {string filePath …

【Docker进阶】镜像制作-用Dockerfile制作镜像(一)

进阶一 docker镜像制作 文章目录 进阶一 docker镜像制作用dockerfile制作镜像dockerfile是什么dockerfile格式为什么需要dockerfileDockerfile指令集合FROMMAINTAINERLABELCOPYENVWORKDIR 用dockerfile制作镜像 用快照制作镜像的缺陷: 黑盒不可重复臃肿 docker…

Python 轻量级定时任务调度:APScheduler

简述 APscheduler (Advanced Python Scheduler),作用为按指定的时间规则执行指定的作业。提供了基于日期date、固定时间间隔interval 、以及类似于Linux上的定时任务crontab类型的定时任务。该框架不仅可以添加、删除定时任务,还可以将任务存储到数据库…

ES6中新增Array.of()函数的用法详解

new Array()方法 ES6为Array增加了of函数用一种明确的含义将一个或多个值转换成数组。因为用new Array()构造数组的时候,是有二意性的。 构造时,传一个参数,实际上是指定数组的长度,表示生成多大的数组。 构造时,传…

问题:媒体查询语法中, 可用设备名参数表示“文档打印或预览“的是 #媒体#媒体#其他

问题:媒体查询语法中, 可用设备名参数表示"文档打印或预览"的是 A、C.?screen B.?projection C、A.?print D.?speech 参考答案如图所示

【JavaEE】UDP协议与TCP协议

作者主页:paper jie_博客 本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。 本文于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造&…

【大厂AI课学习笔记】1.4 算法的进步(4)关于李飞飞团队的ImageNet

第一个图像数据库是ImageNet,由斯坦福大学的计算机科学家李飞飞推出。ImageNet是一个大型的可视化数据库,旨在推动计算机视觉领域的研究。这个数据库包含了数以百万计的手工标记的图像,涵盖了数千个不同的类别。 基于ImageNet数据库&#xf…

如何构建多种系统架构支持的 Docker 镜像

如何构建多种系统架构支持的 Docker 镜像 1.概述2.解决方案3.使用manifest案例 1.概述 我们知道使用镜像创建一个容器,该镜像必须与 Docker 宿主机系统架构一致,例如 Linux x86_64 架构的系统中只能使用 Linux x86_64 的镜像创建容器 例如我们在 Linux…

从3天到3小时,“文思助手”让行业专业写作“文思泉涌”

AI 长文写作能否结合用户所在行业规范与需求,定制化体现专业内容?“文思助手”提供了解决方案。基于大语言模型强大理解和生成能力,通过用户自建知识库、个性化语境调整,能够智能地创作出符合专业要求的个性化长文。 厦门苏哒智能…

移动Web——less

1、less-简介 less是一个CSS预处理器,Less文件后缀是.less。扩充了CSS语言,使CSS具备一定的逻辑性、计算能力注意:浏览器不识别Less代码,目前阶段,网页要引入对应的CSS文件VS code插件:Easy LESS&#xff…