【多线程】设计模式之单例模式

💐个人主页:初晴~

📚相关专栏:多线程 / javaEE初阶


一、什么是设计模式

设计模式好⽐象棋中的 "棋谱". 红⽅当头炮, ⿊⽅⻢来跳. 针对红⽅的⼀些⾛法, ⿊⽅应招的时候有⼀些固定的套路. 按照套路来⾛局势就不会吃亏.
软件开发中也有很多常⻅的 "问题场景". 针对这些问题场景, ⼤佬们总结出了⼀些固定的套路. 按照这个套路来实现代码,就可以保证代码不会太差

按照设计模式编写代码能让代码更加“死板”一些。代码太过“灵活”也不见得是件好事,反而容易出现难以预料的bug“死板”一些能一定程度上提高代码的规范性,减少bug的产生

设计模式与框架:

设计模式:针对代码编写过程中的“软性约束”

(不是强制的,但遵守的话能有一定好处)

框架:针对代码编写过程中的“硬性约束”

(针对一些特定场景,大佬们把基本的代码已经写出来了,大部分逻辑也写好了。留出了一些空位,让程序员在空位上填写一些自定义的逻辑)

二、单例模式

单例模式能保证某个类在程序中只存在唯⼀份实例, ⽽不会创建出多个实例.

在开发的一些场景中,我们希望有的类在一个进程中,不应该存在多个实例(对象),此时就可以使用单例模式,限制某个类只能有唯一实例

  • 这⼀点在很多场景上都需要. ⽐如 JDBC 中的 DataSource 实例就只需要⼀个.
java单例模式具体的实现⽅式有很多. 最常⻅的是 "饿汉" 和 "懒汉" 两种.

1、饿汉模式

饿就是 “迫切”的意思,饿汉模式就是指在类被加载的时候,就会创建出该单例的实例
java">class Singleton{private static Singleton instance=new Singleton();public static Singleton getInstance(){return instance;}
}
public class Main {public static void main(String[] args) {Singleton s1=Singleton.getInstance();Singleton s2=Singleton.getInstance();System.out.println(s1==s2);}
}

这样s1和s2就会指向同一个实例对象,因此上述代码“s1==s2”的值为true了:

只要不再其它代码中NEW这个类,每次需要使用时就通过getInstance来获取实例,那么这个类就是单例的了。
但是不能保证类的使用者会按照规范不去NEW一个新的对象。因此,实现单例模式的核心问题就是 防止 类的使用者不小心 NEW了新对象
这时我们可以通过私有化类的构造方法,使得在类外就无法调用该类的构造方法,也就无法创建实例了:

这时在类外调用构造方法就会报错了:

注意
通过 “反射”或者 “序列化反序列化”等方法都能打破这种单例模式。所以这种单例模式只能避免使用者的 “失误”,不能避免使用者的 “恶意攻击”。不过一般也 不会刻意去规避 恶意攻击,因为代价会比较大。
  • 这种单例模式的前提是在一个进程中,如果有多个java进程,那么自然每个进程都能有一个实例了

2、懒汉模式

可不要被名字误解了,在计算机中,“懒”褒义词,反而意为着效率更高

懒汉模式会推迟创建实例的时机,在第一次使用的时候,才会创建实例

        因为很多时候,我们并不需要像“饿汉模式”那样类一加载就创建实例事实上创建实例也是需要开销的,如果一股脑就把一堆类的实例创建了,会浪费很多的资源

比如打开了一个壁纸网站,这个壁纸网站有着几十个G的图片资源。

  • 饿汉模式:一启动,就把所有的资源都加载到内存里,然后再显示在页面上
  • 懒汉模式:启动之后,只加载一小部分资源(如当前页面内的图片),在用户进行翻页操作后,再加载其它资源

显然懒汉模式效率是更高的。接下来就让我们来看看懒汉模式该如何用代码实现吧。


(1)单线程实现

java">class Singletonlazy{private static Singletonlazy instance=null;public static Singletonlazy getInstance(){if(instance==null){instance=new Singletonlazy();}return instance;}private Singletonlazy(){};
}
public class Main {public static void main(String[] args) {Singletonlazy s1=Singletonlazy.getInstance();Singletonlazy s2=Singletonlazy.getInstance();System.out.println(s1==s2);}
}

        这样,类加载时并不会创建实例。在第一次调用 getInstance 方法时,此时“instance==null”的布尔值就为true,就会进入if分支创建出一个实例。之后再调用 getInstance 方法时就不会重复创建实例,而是直接返回第一次创建的实例了。从而实现了单例的效果

(2)多线程实现

        事实上我们上面写的程序在多线程运行时会产生线程安全问题。因为在 getInstance 方法中涉及了修改操作。在博主的 深入剖析线程安全问题 一文中曾分析过,当多线程涉及修改操作时,由于线程执行的并发性,就容易出现各种bug。下面我们就来分析一下可能出现的一种bug:

        这样就会程序导致创建了两个实例。虽然第二次创建覆盖了 instance 的值,使得第一次创建的实例没有引用指向,很快会被垃圾回收机制回收掉。但事实上这样的代码仍然算是有bug的。

        因为实际的构造方法内部处理if语句,还可能会有很多其它的逻辑。一来过多的逻辑并发执行容易导致各种各样的线程安全问题,二来如果创建实例消耗的资源很多,这样重复创建就会导致消耗的资源翻倍,大大降低运行效率

因此我们应该想办法保证实例创建操作的原子性。最自然的想法就是加锁了:

        这样加锁之后确实解决了线程安全问题。但是当已经NEW过对象后,就不会进入if分支,后续执行就只剩读操作了。此时的getInstance方法不加锁也是线程安全的,其实就没必要加锁了。而且加锁操作是会消耗一定资源的,并且会产生阻塞,十分影响效率。如果按上述这种代码的话,每次调用getInstance方法都会加锁,就会消耗很多无意义的资源,严重影响运行效率了。

        因此,上述代码还应进行优化。既然只有首次调用才会有线程安全问题,才需要加锁,那我们在加锁前先判断一下是否是首次调用不就可以了。而只有首次调用时 instance 的值才为null,一旦 instance 创建好了,值自然就不为null了,此时也不需要加锁了。因此我们可以通过条件 “instance==null” 来判断是否要加锁:

这里,我们会发现竟然连着写了两个同样的if语句,这在我们以往的单线程编程中是令人匪夷所思的。但现在这是在多线程环境下的,任意两个代码中都可能穿插其它线程的逻辑。synchronized 是一个阻塞操作,即使开始时 instance 的值为null,但在阻塞期间,它的值完全有可能会被其它线程给修改,等再恢复执行时,instance 的值就未必是null了。

事实上,这两句if的作用是完全不同的:

  • 外面的if是判段是否要加锁
  • 里面的if是为了判段是否要创建对象

只不过正好在这个代码中,完成上述判断逻辑的语句相同罢了

不过上述代码依旧是存在问题的,可能会因为指令重排序而导致线程安全问题

  • 指令重排序

指令重排序也是编译器优化的一种方式。编译器可能会为了方便在不影响运行结果的情况下改变指令的执行顺序。如果是单线程代码,编译器一般都能做出准确的判断。但如果是多线程的话,编译器就可能会误判从而导致线程安全问题了。

比如上面这句代码,在实际运行中会执行三个指令:

(1)分配内存空间

(2)执行构造方法

(3)把内存空间地址赋值给变量

编译器可能会按照123的顺序执行,也可能按照132的顺序执行。对于单线程代码来说这并不会影响执行结果,但对多线程就可能会出现问题了:

这时, s 就会被赋值一块未初始化的内存,此时一旦调用任何 s 中的方法,都会发生错误。从而导致产生许多难以预料的bug。

这时我们就可以用之前小编在 深入剖析线程安全问题 一文中提到的 volatile 来解决这个问题

可以给 instance 变量加上 volatile 修饰:

这样,编译器就会知道 instance 变量是易失的,后续对这个变量的优化就会变得非常克制了。不仅在读取变量上克制,在修改变量上也会变得非常克制。会禁止对 instance 赋值的操作插入到其它操作之间,就不会出现123执行顺序变成132的情况了,也就不会出现上述因指令重排序而造成的bug了

javavolatile有两个功能:

(1)保证内存可见性

(2)禁止指令重排序(针对赋值)

因此饿汉模式的多线程实现的完全体就是这样:

java">class Singletonlazy{private volatile static Object locker=new Object();private static Singletonlazy instance=null;public static Singletonlazy getInstance(){// 在这里判断当前是否要加锁if(instance==null){synchronized (locker){if(instance==null){instance=new Singletonlazy();}}}return instance;}private Singletonlazy(){};
}

那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊


http://www.ppmy.cn/news/1518622.html

相关文章

记录|如何全局监听鼠标和键盘等事件

目录 前言一、MyMessager类二、Form中进行Timer监听更新时间 前言 参考文章: C# winfrom 长时间检查不到操作,自动关闭应用程序 本来是想,如果一段时间没有操作软件,这个软件就自动退出的任务。但是在C#中,采用winform…

Google Earth Engine:对NDVI进行惠特克平滑算法进行长时序分析

目录 简介 函数 ee.Array.identity(size) Arguments: Returns: Array transpose(axis1, axis2) Arguments: Returns: Array matrixMultiply(image2) Arguments: Returns: Image matrixSolve(image2) Arguments: Returns: Image arrayFlatten(coordinateLabels, …

Qt应用的高分辨率适配

背景 工作中需要面对触控大屏的4K分辨率场景,同时也有越来越多人开始使用高分屏,原来多基于1080p分辨率开发的Qt程序无法很好适配更高的分辨率。 没有特意针对高分辨率场景做适配时,Qt应用的表现通常有两种情况: 分辨率高的情况…

波导阵列天线单元学习笔记7 一种用直接金属激光烧结考虑的轻质量,宽带,双圆极化波导腔体阵列

摘要: 提出了一种工作在Ku频段的轻质量,宽带,双圆极化波导腔体阵列。为了获得双正交的线极化,基本的辐射单元是由两个波导馈电的方形腔体。通过恰当地对馈网进行调谐,可以获得对于两个正交极化的等辐同相辐射电场&…

智能指针(RAII)

智能指针(RAII) 一、内存泄漏1、介绍2、原因3、泄漏的内存类型分类 二、RAII1、介绍2、基本思想3、优点4、实现方式 三、unique_ptr1、介绍2、主要特性3、注意事项4、unique_ptr类5、示例代码6、运行结果7、简单实现 四、shared_ptr1、介绍2、主要特点3、…

如何处理时间序列异常值?理解、检测和替换时间序列中的异常值

异常值的类型 (欢迎来到雲闪世界) 异常值是与正常行为有显著偏差的观察结果。 时间序列可能会因某些异常和非重复事件而出现异常值。这些异常值会影响时间序列分析,并误导从业者得出错误的结论或有缺陷的预测。因此,识别和处理异常值是确保时间序列建模…

【TDesign】如何修改CSS变量

Tdesign的组件想通过style定义样式没效果, 可以通过组件api文档修改, 组件提供了下列 CSS 变量,可用于自定义样式。 比如Cell, https://tdesign.tencent.com/miniprogram/components/cell?tabapi 提供了: –td-cell-left-icon-color 图标颜色 –td-cell…

【Leetcode 2341 】 数组能形成多少数对 —— 去重

给你一个下标从 0 开始的整数数组 nums 。在一步操作中,你可以执行以下步骤: 从 nums 选出 两个 相等的 整数从 nums 中移除这两个整数,形成一个 数对 请你在 nums 上多次执行此操作直到无法继续执行。 返回一个下标从 0 开始、长度为 2 的…

电脑变声器软件哪个好用?最新款实时变声器数据公开!

电脑变声器软件哪个好用?什么场合下需要用到变声器?在派对或朋友聚会中,使用变声器可以模仿各种动物、名人或虚构角色的声音;直播变声搞怪;匿名游戏聊天;电影、动画、电视音效、旁白制作等等,都…

高职院校大数据分析与可视化微服务架构实训室解决方案

一、前言 随着信息技术的飞速发展,大数据已成为推动社会进步与产业升级的关键力量。为了培养适应未来市场需求的高素质技术技能型人才,高职院校纷纷加大对大数据分析与可视化技术的教学投入。唯众,作为国内领先的职业教育解决方案提供商&…

2 Python开发工具:PyCharm的安装和使用

本文是 Python 系列教程第 2 篇,完整系列请查看 Python 专栏。 1 安装 官网下载地址https://www.jetbrains.com.cn/pycharm/,文件比较大(约861MB)请耐心等待 双击exe安装 安装成功后会有一个30天的试用期。。。本来想放鸡火教程&…

Nginx负载均衡请求队列配置:优化流量管理

在高流量的Web应用场景中,合理地管理进入的请求流量对于保持服务的稳定性和响应性至关重要。Nginx提供了请求队列的配置选项,允许开发者控制进入后端服务器的请求数量。通过配置请求队列,可以在后端服务器达到最大处理能力时,优雅…

005、架构_数据节点

​DN组件总览 ​ DN节点包含进程 dbagent进程:主要提供数据节点高可用、数据导入导出、数据备份恢复、事务一致性、运维类功能、集群的扩缩容、卸数等功能;MySQL进程:主要提供数据一致性、分组管理、快同步复制、高低水位等;

测试岗位应该学什么

以下是测试岗位需要学习的一些关键内容: 1. 测试理论和方法 - 了解不同类型的测试,如功能测试、性能测试、压力测试、安全测试、兼容性测试等。 - 掌握测试策略和测试计划的制定。 2. 编程语言 - 至少熟悉一种编程语言,如 Python、Java…

网络路由介绍,route指令,查询路由表的过程,默认路由

目录 路由 本地主机的路由功能 引入 route指令 查询路由表的过程 介绍 示例 默认路由 注意 路由 本地主机的路由功能 引入 报文经过多个路由器转发至公网,再从公网定位后转发至私网,最终到达目标主机 而报文肯定是要先经过本地主机的 所以本地主机也具有路由功能,也…

django网吧收费管理系统 项目源码26819

摘 要 随着互联网的普及,网吧作为公共互联网接入场所,依旧在许多地区发挥着重要作用。现代网吧不仅仅是提供上网服务的场所,还包括了游戏、社交、休闲等多功能体验。为了提高网吧的服务质量和运营效率,迫切需要一个高效的管理系统…

mysql基础语法——个人笔记

0 前言 以前学习且实践过mysql,但后来用得少,随着岁月更替,对其印象渐浅,所以每次需要用时,都会去再看一眼语法规范,然后才能放心动手操作 然而,在信息爆炸的时代,查语法规范时&am…

ubuntu录屏解决ubuntu下无法播放MP4格式文件的方法

参考 gnome gnome是系统自带的录屏,通过ctrlshiftaltr触发 保存到了视频目录下,webm格式文件。 screencastify 这是一个chrome扩展,,一般不推荐使用 recapp 比gnome自由一些,可以自由屏幕录制。但是无法修改录制…

如何将Dxf文件中的Vertex与相应的polyline关联起来

在处理DXF(Drawing Exchange Format)文件时,将VERTEX和相应的POLYLINE关联起来是一个常见的需求。这通常涉及解析DXF文件中的几何实体,并确保它们之间的关系正确。以下是一些步骤和示例代码,帮助你实现这种关联&#x…

如果学流式系统你想选一本书,那必须是这本

“如果你关心流式处理和批处理工作的正确性,那么这本书是必读的。它对该主题的讨论是我看到的思考最清晰、最合逻辑的,其思想也被精彩诠释。” ——马丁克莱普曼(Martin Kleppmann),剑桥大学 流式系统 如今&#xff0c…