JavaEE:单例模式(饿汉模式和懒汉模式)精讲

前言

什么是单例模式?

其实用通俗的话就是程序猿约定俗成的一些东西,就比如如果你继承了一个抽象类,你就要重写里面的抽象方法,如果你实现了一个接口,你就要重写里面的方法。如果不进行重写,那么编译器就会报错。这其实就是一个规范。

而单例模式能保证某个类在程序中只存在唯一的一个实例,而不会创建出多个实例

那么,单例模式又分成“饿汉”和“懒汉”两种、

一.饿汉模式 

顾名思义,饿汉模式就是在类加载的时候,创建实例。

package thread;
//期待这个类能有唯一实例
public class hungryDemo {private static hungryDemo instance = new hungryDemo();public static hungryDemo getInstance() {return instance;}//把构造方法设置为私有,这样在类外就无法 new 出这个对象的实例了private hungryDemo() {}
}

代码解读:

1. 首先创建了一个 hungryDemo 类,里面有一个类方法和一个类变量

2. 我们将构造方法设置为了private,那么在类外就无法再针对 hungryDemo 再实例化类了


我们现在在类外,通过 hungryDemo提供的  public static hungryDemo getInstance 方法来进行调用,可以发现如下结果:

class Demo1 {public static void main(String[] args) {hungryDemo h1 = hungryDemo.getInstance();hungryDemo h2 = hungryDemo.getInstance();System.out.println(h1 == h2);}
}

运行结果:

可以发现,两者获取到的类对象引用是一致的,那么单例模式的饿汉版本就创建好了。

二.懒汉模式 

🎈单线程版本

我们的预期结果是不变的,那就是要实现单例模式,也就是这个类 只能被实例化一次!!!

那么懒汉模式顾名思义,也就是类加载的时候不创建实例,第一次使用的时候才创建实例。

那么我们可以写出以下代码:

package thread;public class lazyDemo {private static lazyDemo instance = null;  public static lazyDemo getInstance() {/*** 只有调用该方法的时候,才创建对象*/if (instance == null) {instance = new lazyDemo();}               return instance;}private lazyDemo() {}
}

代码解读:

🍺首先,设置类成员变量 instance 为 null,当第一次使用getInstance()的时候才进行创建对      象。

🍺其次,跟饿汉模式一样,将类的构造方法设置为 private ,类外无法再次创建对象。

🍺最后,在getInstance方法中判断 instance 是否为空,为空那就创建对象。为空说明已经        被调用一次了,那么就直接返回 instance 引用。

🎈🎈多线程版本 1

在以上的单线程版本中,我们不难发现以下问题:

假设现在有两个线程,他们是按照如下的顺序来执行的:

那么此时的代码就会出现问题: t1 线程首先判断了 instance 是否为空,此时 t2 线程来运行了,也判断 instance 是否为空,紧接着 instance不为空,然后就创建了对象! 然后再回到 t1 线程中,又要进行创建对象。  此时问题已经很明显了,那就是 由于if代码块在多线程中的执行顺序问题导致的

更精简一下:

就是 instance = new lazyDemo() 是写操作, instance == null 是读操作,在多线程中,如果一段代码即涉及读操作,又设计写操作,那么就很容易出现问题!!!


🍭解决办法:

  当一段代码是因为读写操作出BUG,我们首先想到的就是加锁。也就是在我写的时候,你不要       读。我读的时候,你不要写。

synchronized 是一种内置的 Java 关键字,它用于实现线程的同步。当一个线程进入synchronized块或方法时,它获得了锁,这会阻止其他线程同时进入相同的synchronized块或方法,从而确保了共享资源的互斥访问。

修改代码如下:

package thread;public class lazyDemo {private static lazyDemo instance = null;  public static lazyDemo getInstance() {/*** 只有调用该方法的时候,才创建对象*/synchronized (lazyDemo.class) {   //1. 加锁解决的是线程安全问题(确保是单例模式,只new一次)if (instance == null) {instance = new lazyDemo();}}return instance;}private lazyDemo() {}
} 

对于对象lazyDemo.class,实际上就是lazyDemo这个类,也就是对类进行加锁。

此时加锁之后,当t1线程进行读写操作的时候,t2线程再次进行访问就只能进行阻塞。

此时t1就可以放心创建出一个对象出来,此时t2再进行调用方法的时候,instance 不为空,就直接返回 t1 创建好的对象引用。 这时候就确保了只创建出一个实例。

🎈🎈🎈 多线程版本2

其实,多线程版本1 还是有问题的,我们发现:如果t1 线程加锁后创建好了对象,其他线程(t2,t3,t4.........)在进行访问的时候,首先就要进行加锁操作。 也就是每次访问都要进行加锁,这是一个资源开销非常大的操作。

深入探究一下,我们发现其他线程(t2,t3,t4.........)在进行访问的时候,只需要判断当前的对象是否被创建好了即可。如果被创建好了,那么就直接返回对象引用。如果没有被创建好,再进行加锁创建对象。

修改代码如下:

public class lazyDemo {private static lazyDemo instance = null; public static lazyDemo getInstance() {/*** 只有调用该方法的时候,才创建对象*/if(instance == null) {   //2. if判断解决的是多次加锁,加锁频率太高的问题synchronized (lazyDemo.class) {   //1. 加锁解决的是线程安全问题(确保是单例模式,只new一次)if (instance == null) {instance = new lazyDemo();}}}return instance;}private lazyDemo() {}
}

在多线程中,这两个 if 的作用大不相同!!!

修改后,我们发现如果 t1 线程创建好了对象, 此时其他线程(t2,t3,t4.........)在进行调用的时候,首先判断了instance 是否为空,不为空就说明已经创建好了对象~

🎈🎈🎈🎈多线程版本3

其实到现在,这个懒汉模式的单例代码还是有问题!!!

 在多线程下,要考虑到编译器的优化问题,当编译器没有按照我们的逻辑进行操作的时候,那么就会出现问题。

在此代码中,new 操作可以分为以下三步:

1.申请内存空间(一定先执行),获取到内存地址  

2.在内存空间上构造对象(构造方法)

3.把内存的地址,赋值给 instance 引用

在单线程环境下,执行那种顺序都无所谓,但是如果在多线程环境下,就可能出现问题:

假设是按照 1 3 2 的顺序来执行,当 1 和 3 操作执行完的时候,instance 已经非空了,只是内存空间上还没有构造对象 / 方法,此时instance 指向的是一个还没初始化的非法对象。 此时此刻 t2 进行访问,判断 instanc 是不为空的,然后就返回了一个还没初始化的非法对象,进一步 t2 线程就有可能访问 instance 里面的属性和方法。此时就出现了问题了。

这个问题就是指令重排序问题,解决办法就是让 instance 加入上volatile 关键字,此时就避免了指令重排序问题。

    //3.加 volatile是为了解决new 操作的指令重排序问题    
private volatile static lazyDemo instance = null; 

此时的代码就会严格按照 1  2  3 的顺序执行。


总结:单例模式是一个约定俗成的规范,保证一个类只能实例化一个对象。饿汉模式在多线程和单线程都没有问题,因为一开始它就创建好了对象。 而懒汉模式的多线程版本会出现以下三个问题:1. 线程安全问题( 确保只new 一次)2. 多次重复加锁的问题  3. 指令重排序问题。

希望以上的解决办法对你有所帮助!!!

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

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

相关文章

Android开发中十六进制字符串的各种转换

在我们进行开发中,会遇到将16进制转换为各种形式,今天我们就详细的讲解一下: 1、十六进制字符串转byte[] public static byte[] hexStringToBytes(String hexString) {if (hexString null || hexString.equals("")) {return nul…

IDEA版SSM入门到实战(Maven+MyBatis+Spring+SpringMVC) -Spring管理第三方bean

第一章 Spring管理第三方bean 1.1 Spring管理druid步骤 导入jar包 <!--导入druid的jar包--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.10</version></dependency><!-…

2024上海智慧城市展会(世亚智博会)促进长三角地区智慧城市发展

上海市政府近期印发的《上海市进一步推进新型基础设施建设行动方案(2023-2026年)》标志着新一轮新基建的全面启动。市政府副秘书长、市发展改革委主任顾军指出&#xff0c;这一行动方案紧抓智能算力、大模型、数据要素、区块链、机器人等技术发展趋势和绿色低碳节能要求&#x…

ceph 12版本命令

目录 一、osd操作相关删除osd调整OSD的恢复等级 二、host操作相关删除没有osd的host将host移动到对应的bucket中 一、osd操作相关 删除osd ceph osd down <X> ceph osd out <X> ceph osd rm <X> ceph auth del <osd.X> ceph osd crush rm <osd.X&…

01 python 入门语法

1.1 python中的自变量 # 定义一个变量来记录钱包的余额 money 50 # 通过print语句来展示&#xff08;输出&#xff09;变量记录的内容 print(钱包还有:,money) # 买花花费10元 money - 10 # 输出钱包剩余多少钱 print(钱包还有:,money)# 假设一小时就输出一次钱包余额 &#…

【LeetCode:2132. 用邮票贴满网格图 | 二维前缀和 + 二维差分和】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

达索系统SOLIDWORKS 2024 Visualize新功能

SOLIDWORKS Visualize&#xff08;原名为 Bunkspeed&#xff09;是一整套独立的软件工具&#xff0c;Visualize模块主要是用于对SOLIDWORKS设计出的产品图进行渲染、做动画&#xff0c;方便用户更好的展示、宣传产品&#xff1b;以最快速、最轻松的方式创建专业的照片级图像、动…

基于YOLOv8深度学习的水稻害虫检测与识别系统【python源码+Pyqt5界面+数据集+训练代码】目标检测、深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

代码随想录Day51—— 309.最佳买卖股票时机含冷冻期 714.买卖股票的最佳时机含手续费

309.最佳买卖股票时机含冷冻期 给定一个整数数组prices&#xff0c;其中第 prices[i] 表示第 i 天的股票价格 。​ 设计一个算法计算出最大利润。在满足以下约束条件下&#xff0c;你可以尽可能地完成更多的交易&#xff08;多次买卖一支股票&#xff09;: 卖出股票后&…

《使用ThinkPHP6开发项目》 - 登录接口三【表单验证】

《使用ThinkPHP6开发项目》 - 登录接口一-CSDN博客 https://blog.csdn.net/centaury32/article/details/134974860 在设置用户登录时&#xff0c;由于安全问题会对登录密码进行加密 表单验证这里也可以使用ThinkPHP6自带的验证规则&#xff0c;创建一个验证管理员的文件 ph…

MYSQL各种日志

感谢B站up主的视频分享 黑马程序员 MySQL数据库入门到精通&#xff0c;从mysql安装到mysql高级、mysql优化全囊括_哔哩哔哩_bilibili

IDEA版SSM入门到实战(Maven+MyBatis+Spring+SpringMVC) -Spring依赖注入方式

第一章 Spring依赖注入方式【基于XML】 为属性赋值方式 通过xxxset()方法通过构造器反射 1.1 set注入 语法&#xff1a;<property> 1.2 构造器注入 语法&#xff1a;<constructor-arg> 1.3 p名称空间注入 导入名称空间&#xff1a;xmlns:p“http://www.springf…

2023 亚马逊云科技 re:Invent 大会探秘:Aurora 无限数据库的突破性应用

文章目录 一、前言二、Amazon Aurora 无限数据库2.1 亚马逊云科技数据库产品发展历程2.2 什么是 Amazon Aurora Limitless Database&#xff08;无限数据库&#xff09;2.3 Amazon Aurora Limitless Database 设计架构2.4 Amazon Aurora Limitless Database 分片功能2.5 使用 A…

xtu oj 1194 Recipient

题目描述 快递小哥每天都辛苦的送快递&#xff0c;今天他需要送N份快递给N个收件人&#xff0c;第i份快递需要送给第i个收件人。 请问其中发生恰好K个送错了的情况数是多少&#xff1f; 输入 存在多样例。 每行输入两个整数N和K&#xff0c;1≤N≤1000,0≤K≤N。 如果两个都…

pytorch中的归一化:BatchNorm、LayerNorm 和 GroupNorm

1 归一化概述 训练深度神经网络是一项具有挑战性的任务。 多年来&#xff0c;研究人员提出了不同的方法来加速和稳定学习过程。 归一化是一种被证明在这方面非常有效的技术。 1.1 为什么要归一化 数据的归一化操作是数据处理的一项基础性工作&#xff0c;在一些实际问题中&am…

SpringBoot - 事件机制使用详解(ApplicationEvent、ApplicationListener)

SpringBoot - 事件机制使用详解&#xff08;ApplicationEvent、ApplicationListener&#xff09; Spring 事件机制使用观察者模式来传递事件和消息。我们可以使用 ApplicationEvent 类来发布事件&#xff0c;然后使用 ApplicationListener 接口来监听事件。当事件发生时&#…

程序员月经焦虑 :如何成为高级工程师

高级工程师意味着什么&#xff1f; ChatGPT的回复&#xff1a;高级工程师对编程语言、软件设计原则和开发方法有深刻的理解。 开发方法 开发方法学是旨在使团队有效的组织方法。这些对我们来说可能很无聊&#xff0c;但我们希望你在这方面有专业知识。 我已经对非敏捷开发方法…

SAHI强化YOLOv5在小目标上的表现

文章目录 环境前言安装sahiyolov5检测sahi添加新的检测模型 环境 ubuntu 18.04 64bitsahi 0.8.4yolov5 5.0pytorch 1.7.1cu101 前言 目标检测和实例分割是迄今为止计算机视觉中最重要的应用领域&#xff0c;各种目标检测网络层出不穷&#xff0c;然而&#xff0c;小目标的检…

速卖通(AliExpress)店铺流量怎么转化?自养号测评策略

随着全球电商的蓬勃发展&#xff0c;速卖通&#xff08;AliExpress&#xff09;作为中国领先的跨境电商平台&#xff0c;为卖家提供了一个广阔的销售舞台。然而&#xff0c;对于卖家来说&#xff0c;如何让速卖通店铺实现转化&#xff0c;吸引更多的买家成为关键。 一、速卖通…

Docker 与 Podman:揭示容器编排的最佳 25 大常见问题解答

让我们告诉你一件事。 这不仅仅是这两个强大平台之间的普通比较。 相反&#xff0c;我们分析并列出了有关 Docker 与 Podman 的最紧迫问题。 但这里有一件事——这些问题不仅被技术角度所包围。 我们还深入研究了业务环境&#xff0c;因为我们知道这不仅仅是关于代码。这是…