多线程代码案例-1 单例模式
单例模式
单例模式是开发中常见的设计模式。
设计模式,是我们在编写代码时候的一种软性的规定,也就是说,我们遵守了设计模式,代码的下限就有了一定的保证。设计模式有很多种,在不同的语言中,也有不同的设计模式,设计模式也可以被认为是对编程语言语法的补充。
单例,即单个实例(对象),某个类在一个进程中,只应该创建出一个实例(原则上不应该创建出多个实例),使用单例模式,可以对我们的代码进行一个更为严格的校验和检查。
举个例子:有时候,代码中需要管理/持有大量的数据,此时有一个对象就可以了。比如:我需要一个对象管理10G的数据,如果我们不小心创建出多个对象,内存空间就会成倍地增长。
如何保证只有唯一的对象呢?我们可以选择“君子之约地方式”,写一个文档,文档上约定,每个接手维护代码的程序员,都不能对这个类创建多个实例(很显然,这种约定并不靠谱)我们期望让机器(编译器)能够对代码中的指定类,对创建的实例个数进行检验。如果发现创建出了多个实例,就直接编译报错,但是Java语法中本身没有办法直接约定某个对象能创建出几个实例,那么就需要程序员使用一些技巧来实现这样的效果。
实现单例模式的方式有很多种,这里介绍两种实现方式:饿汉模式和懒汉模式。
1 饿汉模式
代码如下:
//饿汉模式
//期望这个类只能有唯一的实例(一个进程中)
class Singleton{private static Singleton instance = new Singleton();//在这个类被加载时,就会初始化这个静态成员,实例创建的时机非常早——饿汉public static Singleton getInstance(){//其他代码想要使用这个类的实例就需要通过这个方法进行获取,// 不应该在其他代码中重新new这个对象而是使用这个方法获取这个现有的对象return instance;}private Singleton(){//其他代码就没法new了}
}
在这个类中,我们创建出了唯一的对象,被static修饰,说明这个变量是类变量,(由类对象所拥有(每个类的类对象只存在一个),在类加载的时候,它就已经被初始化了)。
而将构造方法设为私有,就使得只能在当前类里面创建对象了,其他位置就不能再创建对象了,因此这个instance指向的对象就是唯一的对象。
其他代码要想使用这个类的实例,就需要通过这个getInstance()方法获取这个对象,而无法在其他代码中new一个对象。
上述代码,称为”饿汉模式“,是单例模式中的一种简单的写法,”饿“形容”非常迫切“,实例在类加载的时候就创建了,创建的时机非常早,相当于程序一启动,实例就创建了。
但是,上面的代码,面对反射,是无能为力的,也就是说,仍然可以通过反射来创建对象,但反射是属于非常规的编程手段,代码中随意使用反射是非常糟糕的。
2 懒汉模式
”懒“这个词,并不是贬义词,而是褒义词。社会能进步,科技能发展,生产效率提高,有很大部分原因都是因为懒。
举个生活中的例子(不考虑卫生):
假如我每次吃完饭就洗碗,那我每次就需要洗全部的碗;但是如果我每次吃完饭把碗放着,等到下次吃饭的时候再洗,此时,如果我只要用到两个碗,那我就只需要洗两个碗就行了,很明显洗两个碗要比洗全部碗更加高效。
在计算机中,”懒“的思想就非常有意思,它通常代表着更加高效。
比如有一个非常大的文件(10GB),使用编辑器打开这个文件,如果是按照”饿汉“的方式 ,编辑器就会先把这10GB的数据都加载到内存中,然后再进行统一的展示。(但是加载了这么多数据,用户还是需要一点一点地看,没法一下子看完这么多)
如果是按照”懒汉“地方式,编辑器就会只读取一小部分数据(比如只读取10KB),把这10KB先展示出来,然后随着用户进行翻页之类的操作,再继续展示后面的数据。
加载10GB的时间会很长,但是加载10KB却只是一瞬间的事情……
懒汉模式,区别于饿汉模式,创建实例的时机不一样了,创建实例的时机会更晚,一直到第一次使用getInstance方法时才会创建实例。
代码如下(注意:这是一个不完整的代码,因为还有一些线程安全问题需要解决~~):
//懒汉的方式实现单例模式class SingletonLazy{private static SingletonLazy instance = null;public static SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}
第一行代码中仍然是先创建一个引用,但是这个引用不指向任何的对象。如果是首次调用getInstance方法,就会进入if条件,创建出对象并且让当前引用指向该对象。如果是后续调用getInstance方法,由于当前的instance已经不是null了,就会返回我们之前创建的引用了。
这样设定,仍然可以保证,该类的实例是唯一一个,与此同时,创建实例的时机就不再是程序驱动了,而是当第一次调用getInstance的时候,才会创建。。
而第一次调用getInstance这个操作的执行时机就不确定了,要看程序的实际需求,大概率会比饿汉这种方式要晚一些,甚至有可能整个程序压根用不到这个方法,也就把创建的操作给省下了。
有的程序,可能是根据一定的条件,来决定是否要进行某个操作,进一步来决定是否要创建实例。
3 单例模式与线程安全
上面我们介绍的关于单例模式只是一个开始,接下来才是我们多线程的真正关键问题。即:上述我们编写的饿汉模式和懒汉模式,是否是线程安全的?
饿汉模式:
//饿汉模式
//期望这个类只能有唯一的实例(一个进程中)
class Singleton{private static Singleton instance = new Singleton();//在这个类被加载时,就会初始化这个静态成员,实例创建的时机非常早——饿汉public static Singleton getInstance(){//其他代码想要使用这个类的实例就需要通过这个方法进行获取,// 不应该在其他代码中重新new这个对象而是使用这个方法获取这个现有的对象return instance;}private Singleton(){//其他代码就没法new了}
}
对于饿汉模式来说,getInstance直接返回instance这个实例,这个操作,本质上就是一个读的操作(多个线程同时读取同一变量,是不会产生线程安全问题的)。因此,在多线程下,它是线程安全的。
懒汉模式 :
//懒汉的方式实现单例模式class SingletonLazy{private static SingletonLazy instance = null;public static SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}
再看懒汉模式,在懒汉模式中,代码中有读的操作(return instance),又有写的操作(instance = new SingletonLazy())。 很明显,这是一个有线程安全问题的代码!!!
问题1:线程安全问题
因为多线程之间是随机调度,抢占是执行的,如果t1和 t2 按照下列的顺序执行代码,就会出现问题。
如果是t1和t2按照上述情况操作,就会导致实例被new了两次,这就不是单例模式了,就会出现bug了!!!
那如何解决当前的代码bug,使它变为一个线程安全的代码呢?
加锁~~
知道要加锁了?那大家不妨想想:如果我把锁像如下代码这样加下去,是否线程就安全了呢?
class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if (instance == null) {//如果首次调用就创建实例sychronized(locker){instance = new SingletonLazy();}}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}
答案很显然:不行!!!因为如上述代码加锁仍然会发生刚才那样的线程不安全的情况。
所以这里如果想要代码正确执行,需要把if和new两个操作,打包成一个原子的操作(即加锁加在if语句的外面)。
class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了synchronized(locker){ if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}} //不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}
此时因为t1拿到了锁,t2进入阻塞,等t1执行完毕后(创建完对象后),t2进行判断,此时因为t1已经创建好了对象,所以t2就只能返回当前对象的引用了。
多线程的代码是非常复杂的,代码稍微变化一点,结论就可能截然不同。千万不能认为,代码中加了锁就一定线程安全,不加锁就一定线程不安全,具体问题要具体分析,要分析这个代码在各种调度执行顺序下不同的情况,确保每种情况都不会出现bug!!!
问题2:效率问题
上述代码还存在的另一个问题是效率问题:试想一下,当你创建完这个单例对象,你每次获取这个单例对象时(是读的操作,并不会有线程问题),每次都要去加锁、解锁,然后才能返回这个对象。(注意:加锁、解锁耗费的空间和时间都是很大的)。
所以为了优化上面的代码,我们可以再加上一层if,如果instance为null(需要执行写操作),考虑到线程安全问题,就需要加锁;如果instance不为null了,就不需要加锁了。
class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if(instance == null){synchronized(locker){ if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}} //不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}
上面的代码,有两重完全相同if判断条件,但是他们的作用是完全不同的:
第一个if是判断是否需要加锁,第二个if是判断是否要创建对象!!!
巧合的是,两个if条件相同,但是他们的作用是完全不同的,这样就实现了双重校验锁。在以后的学习中,还可能出现两个if条件是相反的情况。
问题3:指令重排序问题
这个代码还有一点问题需要解决:我们之前在线程安全的原因中讲过的:指令重排序问题就在懒汉模式上出现了~~
指令重排序,也是编译器优化的一种方式。编译器会在保证逻辑不变的前提下,为了提高程序的效率,调整原有代码的执行顺序。
再举个生活中的例子:
我妈让我去超市买东西:西红柿、鸡蛋、黄瓜、茄子。
超市摊位分布图如下:
如果我按我妈给的顺序,那就会走出这样的路线:
上述方案虽然也能完成我妈给的任务,但如果我对超市已经足够熟悉了,我就能够在保证逻辑不变
的情况下(买到4种菜),调整原有买菜的执行顺序,提高买菜效率:
返回到代码中:
instance = new SingletonLazy();
上面这行代码,可以拆分为三个步骤:
1、申请一段内存空间。
2、调用构造方法,创建出当前实例。
3、把这个内存地址赋给instance这个引用。
上述代码可以按1、2、3这个顺序来执行,但是编译器也可能会优化成1、3、2这个顺序执行。这两种顺序在单线程下都是能够完成任务的。
1就相当于买了个房子
2相当于装修房子
3相当于拿到了房子的钥匙
通过1、2、3得到的房子,拿到的房子已经是装修好的,称为“精装房”;通过1、3、2得到的房子,拿到的房子需要自己装修,称为“毛坯房”,我们买房子时,上面的两种情况都可能发生。
但是,如果在多线程环境下,指令重排序就会引入新问题了。
上述代码中,由于 t1 线程执行完 1 3 步骤(申请一段内存空间,把内存空间的地址赋给引用变量,但并没有进行 2 调用构造方法的操作,会导致 instance指向的是一个未被初始化的对象)之后调度走,此时 instance 指向的是一个非 null 的,但是是未初始化的对象,此时 t2 线程判定 instance == null 不成立,就会直接 return,如果 t2 继续使用 instance 里面的属性或者方法,就会出现问题,引起代码的逻辑出现问题。
那么我们应该如何解决当前问题呢?
volatile关键字
之前讲过volatile有两个功能:
1、保证内存可见性:每次访问变量都必须要重新读取内存,而不会优化为读寄存器/缓存。
2、禁止指令重排序:针对被volatile修饰的变量的读写操作的相关指令,是不能被重排序的。
懒汉模式的完整代码:
//经典面试题!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
package Thread;
//懒汉的方式实现单例模式
//线程不安全,它在多线程环境下可能会创建多个实例
class SingletonLazy{//这个引用指向唯一实例,这个引用先初始化为null,而不是立即创建实例
private volatile static SingletonLazy instance = null;//针对这个变量的读写操作就不能重排序了
private static Object locker;
//第一次if判定是否要加锁,第二次if判定是否要创建对象//双重校验锁public static SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了//加锁效率不高,且容易导致阻塞,所以再加一个判断提高效率if(instance ==null) {//判断是否为空,为空再加锁//不为空,说明是后续的调用就无需加锁了synchronized (locker) {if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}
相关文章:

多线程代码案例-1 单例模式
单例模式 单例模式是开发中常见的设计模式。 设计模式,是我们在编写代码时候的一种软性的规定,也就是说,我们遵守了设计模式,代码的下限就有了一定的保证。设计模式有很多种,在不同的语言中,也有不同的设计…...

CSS实现文本自动平衡text-wrap: balance
不再有排版孤行和寡行 我们都知道那些标题,最后一个单词换行并单独站在新行上,破坏了视觉效果,看起来很奇怪。当然,有老式的 手动换行或将内容分成不同部分。但您听说过text-wrap: balance吗? 通过应用text-wrap: bal…...

mac M芯片运行docker-desktop异常问题
虽然mac已经迭代到m4了,但官方的docker-desktop运行仍然有问题,包括但不限于: 命令行docker找不到docker-desk打不开docker-desktop闪退容器起不来 尝试不同版本后,看到了其他可以在mac跑docker的开源方法,更简单、轻…...
主流数据库运维故障排查卡片式速查表与视觉图谱
主流数据库运维故障排查卡片式速查表与视觉图谱 本文件将主文档内容转化为模块化卡片结构,并补充数据库结构图、排查路径图、锁机制对比等视觉图谱,以便在演示、教学或现场排障中快速引用。 📌 故障卡片速查:连接失败 数据库检查…...

事件响应策略规范模版
事件响应策略 一、事件分级定义 根据事件对业务的影响程度和紧急程度,将事件分为 4个等级(P1-P4),明确各级事件的判定标准:、 二、响应时效承诺 响应时间(从事件确认到首次回复) P1 事件:15 分钟内响应(724 小时电话 / 工单优先接入) P2 事件:30 分钟内响应(工…...
在哪一个终端下运行有影响吗?pip install pillow
在哪一个终端下运行有影响吗?pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn,需要切换到主目录吗? 1. 是否需要切换目录? 不需要切换目录 pip install 安装的包会存放…...
我用 Appuploader绕过 Mac,成功把 iOS 应用上线了 App Store
我以前总觉得,iOS 上架是 macOS Xcode 专属的领域。直到最近项目必须要上架 iOS,团队却没人用 Mac,只能临时组建了一套“跨平台上架流程”。 这篇文章记录我这个“非典型 iOS 开发者”是如何绕开传统 Xcode 流程,借助一系列工具…...
React学习———React Router
React Router React Router 是 React 应用中用于管理路由的流行库,它允许你在单页应用(SPA)中实现导航和页面切换而无需重新加载页面。 安装 npm install react-router-dom核心组件 <BrowserRouter> 使用HTML5的历史记录API&#…...

MGX:多智能体管理开发流程
MGX的多智能体团队如何通过专家混合系统采用全新方法,彻底改变开发流程,与当前的单一智能体工具截然不同。 Lovable和Cursor在自动化我们的特定开发流程方面取得了巨大飞跃,但问题是它们仅解决软件开发的单一领域。 这就是MGX(MetaGPT X)的用武之地,它是一种正在重新定…...
现在环保方面有什么新的技术动态
环保领域的技术发展迅速,尤其在“双碳”目标、数字化转型和可持续发展背景下,涌现出许多创新技术和应用。以下是当前环保领域的新技术动态(截至2024年): 一、碳中和与碳减排技术 CCUS(碳捕集、利用与封存&a…...
归并排序:分治思想的优雅实现
归并排序(Merge Sort)以简洁而高效的分治思想,在众多排序算法中占据着重要的地位。今天,就让我们一同深入探索归并排序的奥秘。 一、归并排序简介 归并排序是一种基于分治策略的排序算法。它的核心思想是将一个大的问题分解成若…...

采购流程规范化如何实现?日事清流程自动化助力需求、采购、财务高效协作
采购审批流程全靠人推进,内耗严重,效率低下? 花重金上了OA,结果功能有局限、不灵活? 问题出在哪里?是我们的要求太多、太苛刻吗?NO! 流程名称: 采购审批管理 流程功能…...

[模型部署] 3. 性能优化
👋 你好!这里有实用干货与深度分享✨✨ 若有帮助,欢迎: 👍 点赞 | ⭐ 收藏 | 💬 评论 | ➕ 关注 ,解锁更多精彩! 📁 收藏专栏即可第一时间获取最新推送🔔…...

Vue3 加快页面加载速度 使用CDN外部库的加载 提升页面打开速度 服务器分发
介绍 CDN(内容分发网络)通过全球分布的边缘节点,让用户从最近的服务器获取资源,减少网络延迟,显著提升JS、CSS等静态文件的加载速度。公共库(如Vue、React、Axios)托管在CDN上,减少…...

接触感知 钳位电路分析
以下是NG板接触感知电路的原理图。两极分别为P3和P4S,电压值P4S < P3。 电路结构分两部分,第一部分对输入电压进行分压钳位。后级电路使用LM113比较器芯片进行电压比较,输出ST接触感知信号。 钳位电路输出特性分析 输出电压变化趋势&a…...
彻底删除Docker容器中的环境变量
彻底删除Docker容器中的环境变量 前言:环境变量的重要性第一步:创建实验容器第二步:验证环境变量第三步:定位容器"身份证"第四步:修改"出生证明"(重要!)第五步:验证手术成果技术原理深度剖析更安全的替代方案常见问题解答结语:知其然更要知其所以…...

使用 gcloud CLI 自动化管理 Google Cloud 虚拟机
被操作的服务器,一定要开启API完全访问权限,你的电脑安装gcloud CLI前一定要先安装Python3! 操作步骤 下载地址,安装大概需要十分钟:https://cloud.google.com/sdk/docs/install?hlzh-cn#windows 选择你需要的版本&a…...

SQL语句,索引,视图,存储过程以及触发器
一、初识MySQL 1.数据库 按照数据结构来组织、存储和管理数据的仓库;是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合; 2.OLTP与OLAP OLTP( On-Line transaction processing )翻译为联机事务处理&am…...
在Web应用中集成Google AI NLP服务的完整指南:从Dialogflow配置到高并发优化
在当今数字化客服领域,自然语言处理(NLP)技术已成为提升用户体验的关键。Google AI提供了一系列强大的NLP服务,特别是Dialogflow,能够帮助开发者构建智能对话系统。本文将详细介绍如何在Web应用中集成这些服务,解决从模型训练到高并发处理的全套技术挑战。 一、Dialogflow…...

7. 进程控制-进程替换
目录 1. 进程替换 1.1 单进程版: 1.2 进程替换的原理 1.3 多进程版-验证各种程序替换接口 2. 进程替换的各种接口 2.1 execl 2.2 execlp 2.3 execv 2.4 execvp 2.5 execle 1. 进程替换 上图为程序替换的接口,之后会详细介绍。 1.1 单进程版&am…...

理解 C# 中的各类指针
前言 变量可以理解成是一块内存位置的别名,访问变量也就是访问对应内存中的数据。 指针是一种特殊的变量,它存储了一个内存地址,这个内存地址代表了另一块内存的位置。 指针指向的可以是一个变量、一个数组元素、一个对象实例、一块非托管内存…...

真题卷001——算法备赛
蓝桥杯2024年C/CB组国赛卷 1.合法密码 问题描述 小蓝正在开发自己的OJ网站。他要求用户的密码必须符合一下条件: 长度大于等于8小于等于16必须包含至少一个数字字符和至少一个符号字符 请计算一下字符串,有多少个子串可以当作合法密码。字符串为&am…...
Qt图表库推荐指南与分析
目录 一、核心图表库横向对比1. Qt Charts2. QCustomPlot3. QWT (Qt Widgets for Technical Applications)4. KD Chart二、性能与功能对比矩阵三、选型策略与组合方案1. 通用型需求:2. 技术型场景:3. 企业级开发:四、未来趋势与避坑指南1. 协议风险:2. 技术兼容性:3. 性能…...
【Dv3Admin】工具视图配置文件解析
在开发后台管理系统时,处理复杂的 CRUD 操作是常见的需求。Django Rest Framework(DRF)通过 ModelViewSet 提供了基础的增删改查功能,但在实际应用中,往往需要扩展更多的功能,如批量操作、权限控制、查询优化等。dvadmin/utils/viewset.py 模块通过继承并扩展 ModelViewS…...

Vue3中实现轮播图
目录 1. 轮播图介绍 2. 实现轮播图 2.1 准备工作 1、准备至少三张图片,并将图片文件名改为数字123 2、搭好HTML的标签 3、写好按钮和图片标签 编辑 2.2 单向绑定图片 2.3 在按钮里使用方法 2.4 运行代码 3. 完整代码 1. 轮播图介绍 首先,什么是…...
C#中UI线程的切换与后台线程的使用
文章速览 UI线程切换示例 后台线程使用示例 两者对比适用场景Application.Current.Dispatcher.InvokeTask.Factory.StartNew 执行同步性Application.Current.Dispatcher.InvokeTask.Factory.StartNew 一个赞,专属于你的足迹! UI线程切换 在WPF应用程序…...

微信小程序 自定义图片分享-绘制数据图片以及信息文字
一 、需求 从数据库中读取头像,姓名电话等信息,当分享给女朋友时,每个信息不一样 二、实现方案 1、先将数据库中需要的头像姓名信息读取出来加载到data 数据项中 data:{firstName:, // 姓名img:, // 头像shareImage:,// 存储临时图片 } 2…...
优艾智合机器人助力半导体智造,领跑国产化替代浪潮
在全球半导体产业加速自动化转型的背景下,传统物流已成为制约智能化升级的关键瓶颈。作为中国移动机器人行业的领军企业,优艾智合(YOUIBOT)自2017年起就敏锐洞察到"半导体设备国产化"的紧迫需求,依托在工业移…...
关于 TCP 端口 445 的用途以及如何在 Windows 10 或 11 上禁用它
TCP 端口 445 主要用于直接通过 TCP/IP 访问 Microsoft 网络,无需使用 NetBIOS 层。此服务自 Windows 2000 和 Windows XP 开始在 Windows 中提供。在 Windows NT/2K/XP 中,SMB(Server Message Block)协议用于文件共享等。它在 Windows NT 中运行在 NetBT(NetBIOS over TC…...
C语言_coredump深度解析
在 C/C++ 开发中,Core Dump 是解决程序崩溃问题的重要手段。本文将系统介绍 Core Dump 的概念、用途、常见原因及调试方法,结合具体代码示例,帮助开发者快速定位和解决问题。 一、Core Dump 是什么? Core Dump(核心转储)是操作系统在进程异常终止时,将进程的内存镜像、…...