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

news2024/11/26 21:38:11

💐个人主页:初晴~

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


一、什么是设计模式

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

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

设计模式与框架:

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

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

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

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

二、单例模式

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

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

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

1、饿汉模式

饿就是 “迫切”的意思,饿汉模式就是指在类被加载的时候,就会创建出该单例的实例
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)单线程实现

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了

java中volatile有两个功能:

(1)保证内存可见性

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

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

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.coloradmin.cn/o/2092406.html

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

相关文章

【微服务】接口的幂等性怎么设计?

一、什么是幂等? 幂等性:短时间内,对于相同输入的请求,无论进行多少次重复操作,都应该和单次调用的结果一致。 二、幂等问题产生的原因是什么?(或者说为什么需要实现幂等性?) 1、前端重复提交 在用户注…

高频Postman接口测试面试题

一、Postman在工作中使用流程是什么样的? 新建集合管理根据接口所属的模块,在集合中不同模块下编写接口测试用例处理接口之间的数据关联操作添加环境变量在tests tab下中增加断言调试接口,确保接口能被正常调用批量运行用例或者导出通过Newm…

STM32H750VBT6烧录源码无反应的问题

当烧录后出现这种情况下,点击魔术棒里面 Linker,勾选第一个方框后再次烧录即可。

【机器学习】聚类算法的基本概念和实例代码以及局部度量学习的概念和实例代码

引言 聚类算法在许多领域都有广泛的应用,例如数据挖掘、生物信息学、图像处理等。 文章目录 引言一、聚类算法1.1 K-Means算法1.2 DBSCAN算法1.3 层次聚类(Hierarchical Clustering)算法1.4 高斯混合模型(Gaussian Mixture Model&…

Python系统教程02

Python 中基本运算符的使用变量基本运算符和变量编写简单的 Python 程序 一、Python 中的加法、减法、乘法、除法、 1.1 Python 中的""运算符 "" 可以用来计算两个数的和 "" 可以用来拼接 运算符可以用来计算两个数的和运算符可以连接多个字符…

Leetcode面试经典150题-36-有效数独升级版-37.解数独

解法都在代码里,不懂就留言或者私信,比第一题稍微难点 public static void solveSudoku(char[][] board) {/**定义三个二维数组分别代表行、列、桶(每9个格子)*/boolean[][] rowExists new boolean[9][10];boolean[][] colExist…

不用U盘重装win10/11

创建适用于 Windows 的安装介质 Windows 10 Windows 8.1 Windows 7 Microsoft 365 免费试用版正在等待你使用 立即解锁 你可以使用安装介质(U 盘或 DVD)来安装 Windows 的新副本、执行全新安装或重新安装 Windows。 要创建安装介质,请转到…

街机 CAPCOM CPS2 中英文名字与驱动对照表

Part.I 简介 本文列举了街机 CPS2 中游戏的中英文名字与其驱动的对照,以帮助诸位更快地找到自己想玩的游戏。 注意:汉化版的街机模拟器 Kawaks 中游戏的中文名字是根据英文直译的,并不是习惯性的中文叫法。比如『三国志』英文名为『Warriors…

安全入门day.03

一、知识点 1、抓包技术应用意义 在渗透安全方面,通过抓包分析,安全人员可以模拟黑客的攻击行为,对系统进行渗透测试。这种测试有助于发现系统中存在的安全漏洞和弱点。一旦发现漏洞,可以立即采取措施进行修复,从而增…

Selenium实战技巧-多页面和Windows控件处理

01 多页面处理 做UI自动化的时候常常会遇到浏览器弹出新的Tab页,或者需要在多个网页服务之间来回取数据的情况。 比如在首页点击文章“Jmeter使用?”的链接,浏览器会弹出一个新的页面显示“Jmeter使用?”这篇文章的详情。此时如…

SpringBoot教程(二十七) | SpringBoot集成AOP实现异常处理

SpringBoot教程(二十七) | SpringBoot集成AOP实现异常处理 前言第一步:统一接口返回结果1. 统一封装结果包含如下参数2. 创建 枚举HttpStatusEnum(返回结果代码)3. 创建 ResponseResult (返回实体类&#x…

如何使用vcftools提取特定的染色体

起源是由于bam文件没有过滤完全,导致calling出来的vcf文件还有线粒体中的染色体存在,因为在金标准文件中只有1-22号和X染色体,不包含线粒体和Y染色体,因为我使用的金标准文件是来自NA12878,是一位白种人女性。因此&…

VBA代码解决方案第十七讲:如何选择一个工作表,选择多个工作表

《VBA代码解决方案》(版权10028096)这套教程是我最早推出的教程,目前已经是第三版修订了。这套教程定位于入门后的提高,在学习这套教程过程中,侧重点是要理解及掌握我的“积木编程”思想。要灵活运用教程中的实例像搭积木一样把自己喜欢的代码…

基于小程序的学习交流论坛的设计与实现(代码+教程)

我们将制作一个具备帖子分类、发帖、搜索、点赞回复、学习小组组建以及用户登录等功能的小程序。下面将详细阐述每个功能的实现方法,并提供一些关键代码片段作为参考。 需求 帖子分类:对用户发布的帖子分类到对应的专区(寻人寻物&#xff0…

算法-最长连续序列

leetcode的题目链接 这道题的思路主要是要求在O(n)的时间复杂度下,所以你暴力解决肯定不行,暴力至少两层for循环,所以要在O(n)的时间复杂度下,你可以使用HashSet来存储数组,对于每个数字&#…

分页查询--条件查询

使用pagehelper插件 我们在pom.xml文件中加入下面的语句&#xff0c;可以使用插件&#xff0c;进行分页查询 <dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1…

最管用的能屏蔽WIndows10/11系统功能按键的工具--powerToys键盘管理器

最近在开发中碰到需要屏蔽系统按键功能的需求&#xff0c;原本以为是程序里屏蔽按键&#xff0c;结果&#xff0c;原来是需要屏蔽操作系统默认按键功能。 这样的话&#xff0c;就只能往注册表&#xff0c;脚本&#xff0c;全局钩子函数&#xff0c;以及一些第三方的什么工具之…

工业智能物联网关,智慧医疗生态圈的创新驱动

项目背景 智慧化数字医疗正在推动医疗健康领域的转型&#xff0c;预计到2024年&#xff0c;全球数字医疗市场规模将达到3656.7亿美元&#xff0c;中国市场规模将增至4130亿元人民币&#xff0c;随着技术的持续创新和市场需求的不断增长&#xff0c;这一领域的需求和潜力将持续扩…

24最新Stable Diffusion入门指南(看完必会)超全面

前言 今天写这个帖子是带大家了解一款强大的 AI 绘画工具——Stable Diffusion&#xff0c;可以帮你解决很多应用层面的[AI控图]问题。 关于 Stable Diffusion 的内容很多&#xff0c;在本篇教程里&#xff0c;我会先为你介绍 Stable Diffusion 模型的运行原理、发展历程和相…

探索离线AI知识库的技术突破:AntSKPro AI 离线知识库一体机

在当今数字化时代&#xff0c;离线AI解决方案变得越来越重要&#xff0c;特别是在网络连接不稳定或不可用的情况下。最近&#xff0c;我有幸接触到一款名为AntSKPro AI 离线知识库一体机的设备&#xff0c;它展示了在离线环境下如何实现强大的AI支持。下面我将分享一些关于这款…