《javaEE篇》--单例模式详解

embedded/2024/10/22 5:17:12/

目录

单例模式

饿汉模式

懒汉模式

懒汉模式(优化) 

指令重排序

 总结

单例模式

单例模式属于一种设计模式,设计模式就好比是一种固定代码套路类似于棋谱,是由前人总结并且记录下来我们可以直接使用的代码设计思路。

单例模式就是,在有些场景中希望一个类只能有一个对象,不能有多个,这时你可能会觉得,这还不简单,我保证自己只new一个对象不就好了,但是你能保证自己只new一个对象,但是你能保证别人不会new对象吗?又或者,你真的能确保自己只new一个对象吗?所以“我保证自己只new一个对象不就好了”,这只是一个君子协定,光靠人是非常不靠谱的,所以我们要依靠计算机来帮助我们实现这个协定。

//这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.

单例模式的实现方式可以分为两种饿汉模式懒汉模式

饿汉模式

具体实现方法

  1. 用static修饰,在类的内部创建一个现成的实例,让对象在在类加载时就被创建
  2. 提供一个方法当需要这个对象时就通过这个方法获得
  3. 用private修饰构造方法,这样外界就不能实例化这个类了

通过这三个方法就可以保证只有一个该类对象了

java">class Singleton{//创建时时机比较早(饿汉模式)//当类被加载时就会执行这里的创建实例操作private static Singleton instance = new Singleton();//后续需要这个对象,都通过这个方法获取public static Singleton getInstance(){return instance;}//私有构造方法private Singleton(){}
}
public class Demo17 {public static void main(String[] args) {//把构造方法设置为私有,之后就无法实例化这个对象了,这能通过刚刚创建的方法//Singleton singleton = new Singleton();Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();//这两个对象其实是一样的System.out.println(s1 == s2);}
}

 通过运行结果我们可以验证,上述在main方法中创建的对象是同一个,也就是刚刚在类中创建的那一个对象

这种方法会在类加载时就把对象创建好,如果不使用就会造成资源浪费

懒汉模式

具体实现

  1. 用static修饰,只声明类型创建实际对象
  2. 私有构造方法
  3. 提供外界用来获得对象的方法,设定当第一次调用方法时才创建对象
java">class SinngletonLazy{//在第一次调用时创建对象(懒汉模式)//只声明一个类型不创建实际对象private static SinngletonLazy instance = null;public static SinngletonLazy getInstance(){//如果是第一次调用,就创建实例对象,否则直接返回对象if(instance == null){instance = new SinngletonLazy();}return instance;}//私有构造方法private SinngletonLazy(){}
}

 饿汉模式懒汉模式的最大区别就是,饿汉模式在类加载时就创建了对象,懒汉模式在第一次调用方法时才创建对象,假如我们要打开一本电子书,饿汉模式是要把整本书都加载完成才可以看,而懒汉模式则是在翻页时才加载下一页,可想而知懒汉模式是更高效的。

懒汉模式(优化) 

不过上述实现懒汉模式的方法只在单线程下才适用,如果是在多线程环境下会引起线程安全问题。

这里我来画图解释一下

如果当t1执行到①时,if判定为真,程序将要进到if语句内部,此时t2线程插了进来,由于t1线程还没创建出对象,所以t2线程的if语句也会判定为真,接着t2线程new了一个对象然后返回,最后又回到t1线程这边紧接着又new了一个对象,此时代码就出现问题了。

我们第一时间想到的方法就是直接给方法加上锁,这个方法确实可以解决问题,但是这样的话,代码的并发性就降低了,进而影响到代码的效率,所以我们要换种思路。

可以思考一下,我们的加锁操作只是再第一次创建对象时才需要,所以我们在加锁操作前再加一个if语句来判断当前是否需要加锁,这样就既实现了懒汉模式,又优化了代码的效率

java">class SinngletonLazy{//在第一次调用时创建对象(懒汉模式)//只声明一个类型不创建实际对象private static SinngletonLazy instance = null;public static SinngletonLazy getInstance(){//如果是第一次调用,就加锁创建对象,否则直接返回对象if(instance == null) {//判断是否需要加锁synchronized (SinngletonLazy.class){if (instance == null) {//判断是否需要new对象instance = new SinngletonLazy();}}}return instance;}//私有构造方法private SinngletonLazy(){}
}

 用简洁的话总结一下就是,因为为了防止资源浪费,所以使用一个if条件判断是否需要创建对象,因为在多线程下会出现线程安全的问题,所以要加锁,又因为只需要再第一次创建对象时才需要加锁,所以为了提升效率,再加一层if判断当前是否需要加锁(这两个if本质上没有什么关系,只是刚好判断条件一样而已)

这种方法也叫做Double Check(双重检验) + Lock(加锁) 

虽然当前的代码已经很完善了但是还是会有指令重排序的问题 

指令重排序

指令重排序也是一种编译器的优化,是编译器为了提高效率,在保证代码逻辑顺序不变的情况下,改变代码的实际顺序。

实际上这里的new操作可以分成三个步骤

  1. 向内存申请空间
  2. 在刚刚申请的空间上构造对象
  3. 把刚刚申请的空间的地址付给instance

 但是后面两个步骤对于编译器来说是可以颠倒的,按照123或者132来执行都是可以的,就看那种效率快,在单线程下是没有问题的但是在多线程下就会出现问题

假设有t1,t2线程,t1线程执行到new操作之后先执行1,3,此时编译器已经把刚刚申请的内存地址付给instance,Instance此时已经是一个非空的了,也就是说,此时instance指向一个还没有初始化的非法对象。但是这个时候还没有执行2操作,假如这时t2线程开始执行,判定第一个instance==Null,条件不成立(因为刚刚t1线程已经给instance赋值了),t2线程就直接会返回instance对象。但是此时,t1还没有到内存上构造出对象,T2线程的代码可能就会访问instance里面的属性了,进而就会引起一些bug。

 这里可能会有疑问了,刚刚不是给new操作加锁了吗?为什么还会再new操作时插入其他线程?这就是刚刚代码的缺陷之处,我们为了提高效率只是给new操作加锁了,但是此时t2线程只是执行到了第一个if条件,还没有涉及到任何锁操作,就更谈不上阻塞等待了。t2线程此时连第一个if条件都没有进去,就拿着一个还没有初始化的非法对象返回了。

解决方法

不过解决方法也很简单,使用volatile关键字修饰就可以了,volatile可以防止指令重排序,相当于告诉编译器,不要进行代码优化,让它按照本来的顺序执行

此外volatile还有一个作用:保证内存可见性,就是每一时刻线程读取到该变量的值都是内存中最新的那个值(线程每次操作该变量都需要先读取该变量)

 最终代码展示:

java">class SinngletonLazy{//在第一次调用时创建对象(懒汉模式)//只声明一个类型不创建实际对象private static volatile SinngletonLazy instance = null;public static SinngletonLazy getInstance(){//如果是第一次调用,就加锁创建对象,否则直接返回对象if(instance == null) {//判断是否需要加锁synchronized (SinngletonLazy.class){if (instance == null) {//判断是否需要new对象instance = new SinngletonLazy();}}}return instance;}//私有构造方法private SinngletonLazy(){}
}

 总结

饿汉模式:在类加载时创建对象,通过方法直接返回该对象,不会出现并发安全问题

懒汉模式:在第一次需要对象是才会创建对象,但会有并发问题,建议使用Double Check(双重检验) + Lock(加锁) 可以很好的解决问题

为了在多线程环境下防止,疑问指令重排序而导致代码出现问题,要使用volatile修饰对象

以上就是博主对单例模式知识的分享,在之后的博客中会陆续分享有关线程的其他知识,如果有不懂的或者有其他见解的欢迎在下方评论或者私信博主,也希望多多支持博主之后和博客!!🥰🥰

下一篇博客博主将分享有关阻塞队列等知识,还希望多多支持一下!!!😊


http://www.ppmy.cn/embedded/86738.html

相关文章

PHP基础语法-Part2

if-else语句、switch语句 与其他语言相同 循环结构 for循环while循环do-while循环foreach循环,搭配数组使用 foreach ($age as $avlue) //只输出值 {xxx; } foreach ($age as $key > $avlue) //键和值都输出 {xxx; }foreach ($age as $key >…

重生之我当程序猿外包

第一章 个人介绍与收入历程 我出生于1999年,在大四下学期进入了一家互联网公司实习。当时的实习工资是3500元,公司还提供住宿。作为一名实习生,这个工资足够支付生活开销,每个月还能给父母转1000元,自己留2500元用来吃…

《昇思25天学习打卡营第19天|基于MobileNetv2的垃圾分类》

基于MobileNetv2的垃圾分类 本文档主要介绍垃圾分类代码开发的方法。通过读取本地图像数据作为输入,对图像中的垃圾物体进行检测,并且将检测结果图片保存到文件中。 1、实验目的 了解熟悉垃圾分类应用代码的编写(Python语言)&a…

【九大高校联合支持,学生易中,已确认ISSN号,见刊检索有保障!】2024年电力电子与电气工程国际学术会议(P3E 2024,8月23-25)

随着科技的不断进步和社会的发展,电气工程在各个领域都发挥着重要作用,它涉及到了电力系统、电子技术、自动控制等多个方面。近年来,人工智能、大数据、云计算等技术也不断更迭,使得电气工程也更加注重智能化、数字化的发展以及结…

Django 表单常用字段参数

Django Form表单,常用表单字段-CSDN博客 在Django中,表单(Form)是用来处理HTML表单数据的重要工具。Django的表单API允许你定义表单字段及其验证规则。每个表单字段都可以通过多种参数来定制其行为。以下是一些常用的表单字段参数…

DNS劫持

目录 一、DNS的基本概念 二、DNS劫持的工作原理 三、DNS劫持的影响 四、DNS劫持的防范措施 DNS劫持:一种网络安全威胁的深入分析 在当今网络日益发达的时代,互联网已经成为了人们日常生活中不可或缺的一部分。然而,随着网络技术的进步&am…

钉钉 ai卡片 stream模式联调

sdk连接 新建卡片模板下载node.js sdkconfig.json 配置应用信息 启动项目npm i npm run build npm run start连接成功 获取卡片回调 注册卡片回调事件调用https://api.dingtalk.com/v1.0/card/instances 创建卡片实例,返回实例Id //参数结构 {"cardTempla…

基于 Electron+Vite+Vue3+Sass 框架搭建

技术参考 技术描述Electron一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。嵌入 Chromium 和 Node.jsElectron Forge用于打包和分发 Electron 应用程序的一体化工具。英文地址在此Vite前端构建工具Vue3用于构建用户界面的 JavaScript 框架vitejs/plugin-vueVite 插…