多线程代码案例 - 1
目录
单例模式
1. 饿汉模式
2. 懒汉模式
单例模式与多线程
问题1
问题2
问题3
完!
单例模式
单例模式是一种设计模式。
设计模式,是我们在编写代码时候的一种软性的规定,也就是说,我们遵守设计模式,代码的下限是有保证的。设计模式有很多种,在不同的语言中,也有不同的设计模式,设计模式也可以被认为是对编程语言语法的补充。
单例 ==》 即单个实例(对象),某个类,在一个进程中,只应该创建出一个实例(是原则上不应该有多个),使用单例模式,可以对我们的代码进行一个更为严格的校验和检查。
举个栗子:有的时候,代码中需要使用一个对象,来管理 / 持有大量的数据,此时有一个对象就可以了。比如,一个对象管理了 10G 的数据,如果我们不小心创建出多个对象,内存空间就会成倍的增长....
唯一的对象是如何保证的呢?我们可以选择“君子之约”的方式,即写一个文档,文档上约定,每个接手维护代码的程序员,都不能把这个类创建多个实例...(很显然,这种约定并不靠谱....)
我们期望让机器(编译器)能够对代码中的指定类,创建的实例个数进行检验。如果发现创建多个实例了,就直接编译报错的这种,如果能做到这一点,我们就可以放心的编写代码了,不会担心因为失误创建出多个实例...
Java 语法中,本身没有办法直接约定某个对象能创建几个实例....就需要一些技巧来实现这样的效果。
实现单例模式的方式有很多种,这里介绍最基础的两种实现方式:1. 饿汉模式 2. 懒汉模式
1. 饿汉模式
我们创建一个类,名为 Singleton 希望这个类在一个进程中,只能有唯一的实例

这个引用,就是我们期望创建出的唯一的实例的引用:

在这行代码中,使用 static 修饰,static 表示静态的,指的是“类属性”,instance 就是 Singleton 类对象里面持有的属性。(类对象是 Singleton.class 从 .class 文件加载到内存中,表示这个类的一个数据结构),每个类的累对象,只存在一个,类对象中的 static 属性,自然也是只有一个了。
因此 instance 指向的这个对象,就是唯一的一个对象。
其他代码要想使用这个类的实例,就需要通过这个方法来进行获取,不应该在其他代码中重新 new 这个对象,而是使用这个方法获取到线程的对象。

之后我们再添加一个无参的 private 的构造方法:

这样下来,就从根本上阻止了其他代码,使得其他代码没有办法 new,只能使用 getInstacne() 方法
上述代码,称为“饿汉模式”,是单例模式中一种简单的写法,所谓 “饿” 形容 "非常迫切",实例是在类加载的时候就创建了,创建的时机非常早,相当于程序一启动,实例就创建了,就使用“饿汉”形容“创建实例非常迫切,时机非常早”...
补充:上面的饿汉模式中,如果面对反射,是无能为力的,也就是说,反射可以再创建对象,但反射属于是非常规的编程手段,代码中随便使用反射是非常糟糕的....
2. 懒汉模式
“懒” 这个词,并不是贬义词,而是褒义词...社会能进步,科技能发展,效率生产力提高,可能部分原因还是因为“懒”。
举个栗子:洗碗。A 和 B 两个人吃饭,A 一般做的是,吃完饭,就立即去洗碗,吃一顿饭,用 4 个碗,就要洗 4 个碗。但是 B 就不一样了,B 在吃完饭之后,就把碗放到一边不管了,等到下次吃饭的时候,需要这个碗的时候,再来洗碗。
B 这种洗碗方式,其实能够提升效率。(不考虑卫生的前提下....)
比如,上一顿饭,用了 4 个碗,但是下一顿的时候,只需要使用 2 个碗,这个时候就只需要洗 2 个碗就可以了,另外 2 个碗还继续放着... --> 洗 2 个碗,比洗 4 个碗,更加高效!!!
在计算机中,”懒“的思想,就非常有意思。
比如有一个非常大的文件(10GB),使用编辑器打开这个文件,如果是按照”饿汉“的方式,编辑器就会先把这 10 GB 的数据都加载到内存中,然后再进行统一的展示...(但即使是加载了这么多数据,用户还是需要一点一点的看,没法一下子看完这么多)
如果是按照”懒汉“的方式,编辑器就会只读取一小部分数据(比如只读取 10KB),把这 10KB 先展示出来,然后随着用户进行翻页之类的操作,再继续读后面的数据...
加载 10GB 的时间会很长,加载 10KB 只是一瞬间的事情...
懒汉模式,区别于饿汉模式,是创建实例的时机不太一样了,创建实例的时机会更晚,直到第一次使用的时候,才会创建实例。
代码实现如下:

第一行代码中,仍然是引用指向的唯一实例,不过这个引用先初始化为 null,而不是立即去创建实例。如果是首次调用 getInstance 方法,那么此时 instance 引用为 null,就会进入 if 条件,从而把实例创建出来。如果是后续再次调用 getInstance,由于 instance 已经不再是 null 了,此时不会进入 if,就直接返回之前创建好的引用了。
这样设定,仍然可以保证,该类的实例是唯一一个,与此同时,创建实例的时机就不再是程序驱动了,而是当第一次调用 getInstance 的时候,才会创建。
而进行第一次调用 getInstance 这个操作的执行时机就不确定了,要看程序的实际需求,大概率要比饿汉这种方式要晚一些,甚至有可能整个程序压根用不到这个方法,也就把创建的操作给省下了。
有的程序,可能是根据一定的条件,来决定是否要进行某个操作,进一步来决定是否要创建实例...

单例模式与多线程
上面我们介绍的关于单例模式只是一个开始,接下来才是我们多线程的真正关键问题。
即:上述我们编写的饿汉模式和懒汉模式,是否是线程安全的?

对于饿汉模式来说,getInstance 直接返回 instance 这个实例,这个操作,本质上就是一个 读 的操作。如果在多线程中,多个线程读取同一个变量,是不是线程安全的?==》 是线程安全的!!!

再来看懒汉模式...在懒汉模式中,代码有读的操作(return instance),也有写的操作(instance = new SingletonLaze())。
问题1
因为多线程之间是随即调度,抢占式执行的,如果 t1 和 t2 按照下列的顺序来执行代码,就会出现问题。

如果是 t1 和 t2 按照上述情况来操作,就会导致实例被 new 了两次,这就不是单例模式了,不符合我们的预期,就有 bug 了。(单例模式的这个对象,可能是一个非常大的对象,可能这个对象要管理 10GB...)
那问题来了,如何改进懒汉模式,让其能够称为线程安全的代码呢? ==》 加锁,synchronized!!!
多线程代码其实是非常复杂的,代码稍微变化一些,结论就可能截然不同。
千万不可以认为,代码中写了 synchronized 就一定线程安全,不写 synchronized 线程就一定不安全,具体问题要具体分析,要分析这个代码在各种调度执行顺序下的不同情况,确保每种情况都不会出现 bug。
这里如果要想代码正确执行,是需要把 if 和 new 两个操作,打包成一个原子的。

多线程下情况:

如果把 synchronized 加在里面,还是无法解决问题,当出现上述情况,t2 仍然会创建一个实例,然后执行完线程,然后解锁,然后 t1 还是可以继续再创建一个实例,结果仍然会创建两个实例 ==》 更加合理的做法是,把 synchronized 套在 if 的外面。

多线程情况:

这种情况下,如果进行了随机调度,但 t2 是阻塞状态的,要等待到 t1 释放锁,这样下来,就可以确保,一定是 t1 执行完 new 操作,执行完修改 instance 之后,再回到 t2 执行 if 操作,此时 t2 的if 条件就不会成立了,t2 就会直接返回了。
问题2
但上述的代码,仍然是存在一些问题的。

如果 instance 已经创建过了,此时后续再调用 getInstance 方法就都是直接返回 instance 实例即可(此处的操作就是纯粹的读操作了,就不会有线程安全问题了),此时,针对这个没有线程安全的代码,仍然我们的上述代码每次调用前都是先加锁,再解锁,此时效率就非常低了!!!
加锁就意味着可能会产生阻塞,一旦线程阻塞,啥时候能解除,就不知道了...(只要一个代码里加了锁,一般和”高性能“就无缘了...) ==》在需要加锁的时候才加锁,不该加锁的时候,不要随便的加锁!!!
所以为了优化上述代码,我们可以再在锁的外面套上一层 if,判定一下这个代码是否需要加锁,如果需要加锁,就加,如果不需要加锁,就不要加...
如果 instance 为 null,则说明是首次使用,首次调用就需要考虑线程安全问题,就需要加锁。
如果 instance 不为 null,就说明是后续的调用,只有读的操作,就不需要加锁了。

上面的代码,有了两重完全相同的 if 判断,我们之前的代码并没有这样写过,是由于我们之前的代码,并不会涉及到阻塞,也不会涉及到多线程,在单线程 / 非阻塞 的代码中连续写两个相同的 if 是没有意义的...
但是在多线程 / 可能阻塞的代码中,这样的代码就是非常有意义的,看起来是两个一样的条件,实际上,两个条件的结果可能是相反的。
第一个 if 判定的是 是否要加锁!
第二个 if 判定的是 是否要创建对象!
巧合的是,两个 if 的条件相同,但是他们的作用是完全不同的,这样就实现了 线程安全 and 执行效率双重校验锁...
问题3
不巧的是,这个代码,仍然有一点问题。

指令重排序,引起的线程安全问题!!!指令重排序,也是编译器优化的一种方式。 ==》 调整原有代码的执行顺序,保证逻辑不变的前提下,提高程序的效率。
举个栗子:A 让 B 去买菜,菜单如下:西红柿,鸡蛋,黄瓜,茄子。
超市如图:

B 如果按照 A 菜单上的顺序去买:

显然是一波三折,那 B 如果对超市十分熟悉了,保证逻辑不变的前提下(买到四种菜),调整原有买菜的执行顺序,提高买菜的效率。显然按照下图的路线,会更快。

换回代码的视角:

上面的这行代码,其实可以拆成三个大的步骤(不是三个指令!!!)
1. 申请一段内存空间
2. 在这个内存上调用构造方法,创建出这个实例
3. 把这个内存地址赋给 instance 引用变量
正常情况下,上述的代码是按照 1 2 3 的顺序来执行的,但是编译器也可能会优化成 1 3 2 的顺序来执行的。无论是 1 2 3 还是 1 3 2,在单线程下,都是可以的。
1 就相当于买了一个房子
2 就相当于给房子装修
3 就相当于我们拿到房子的钥匙
1 2 3 拿到钥匙之后,就得到了装修好的房子,称为“精装房”, 1 3 2,先拿到要是,然后自己负责装修,称为“毛坯房”,我们买房子,上面两种情况都会发生。
但是,如果在多线程下,指令重排序,就可能引入问题了。
t1 按照 1 3 2 的方式来执行这里的 new 操作

上述代码中,由于 t1 线程执行完 1 3 之后,调度走,此时 instance 指向的是一个非 null 的,但是是未初始化的对象,此时 t2 线程判定 instance == null 不成立,就会直接 return,如果 t2 继续使用 instance 里面的属性或者方法,就会出现问题,引起代码的逻辑出现问题。
解决上述问题,核心思路还是我们前面提到的 volatile
volatile 有两个功能:
1. 保证内存可见性 ==》 每次访问变量必须都要重新读取内存,而不会优化到寄存器 / 缓存中
2. 禁止指令重排序 ==》 针对被 volatile 修饰的变量的读写操作的相关指令,是不能被重排序的

这个时候,针对这个变量的读写操作,就不会出现重排序了,此时的执行顺序就一定 1 2 3,也就杜绝了上述问题了!
完!
相关文章:
多线程代码案例 - 1
目录 单例模式 1. 饿汉模式 2. 懒汉模式 单例模式与多线程 问题1 问题2 问题3 完! 单例模式 单例模式是一种设计模式。 设计模式,是我们在编写代码时候的一种软性的规定,也就是说,我们遵守设计模式,代码的下限…...
display:none与visibility的区别
1. 是否占据空间 display: none:元素完全从文档流中移除,不占据任何布局空间。后续元素会“填补”它的位置。visibility:hidden:元素仍占据布局空间,但内容不可见(类似透明占位符)。 2.渲染与性能 displ…...
算法 | 基于蜣螂优化算法求解带时间窗的车辆路径问题(VRPTW)研究(附matlab代码)
基于蜣螂优化算法求解带时间窗的车辆路径问题(VRPTW)研究 🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏 摘要 带时间窗的车辆路径问题(VRPTW)是物流配送中的核心优化难题。本文提出一种基于蜣螂优化算法(Dung Beetle Optimizer, DBO)的求解方法,通过…...
开发体育赛事直播系统主播认证功能技术实现方案
该体育直播系统系统由东莞梦幻网络科技开发,使用 ThinkPHP 作为后端,Vue.js 作为 PC/H5 端框架,Java 和 Objective-C 分别用于安卓和 iOS 开发。 1、前端实现 (Vue.js) <template><div class"anchor-certification">…...
国产三维CAD「皇冠CAD」在汽车零部件领域建模教程:刹车片
本教程深度融合三维皇冠CAD(CrownCAD)的MBD(Model-Based Definition)设计理念,通过参数化建模、智能约束管理、动态装配验证等功能,实现数据驱动设计,精准解决了汽车制动系统中精密制动组件的设…...
基于指针的线程池
使用原线程池 当 push 和 pop的对象过大时,消耗时延过高,需优化线程池 采用 std::move() unique_ptr的方法,能极大的减少时延, 实际就是避免了多次拷贝,直接使用指针。 代码实现 ThreadPool…...
GitHub与Gitee各是什么?它们的区别与联系是什么?
李升伟 整理 GitHub 介绍 GitHub 是一个基于 Git 的代码托管平台,主要用于版本控制和协作开发。它支持多人协作,提供代码托管、问题跟踪、代码审查、项目管理等功能。GitHub 是全球最大的开源社区,许多知名开源项目都在此托管。 主要功能&…...
SpringMvc获取请求数据
基本参数 RequestMapping("save5") ResponseBody public User save5(String name, int age) {User user new User();user.setName(name);user.setAge(age);return user; } 在url中将name与age进行编写,通过框架可以提取url中的name与age,这…...
简述竞赛经历在考研复试中的作用
文章目录 前言拟录取情况baichuicxyAshy佬齐总结回首展望 前言 随着就业形式的不断变化,竞赛奖项在就业中能起到的作用在逐步减弱。想拿到头部大厂实习面试资格,最低要区域赛银牌起步,当然这也仅仅是面试资格。 那么,竞赛经历在…...
大语言模型开发框架——LangChain
什么是LangChain LangChain是一个开发由语言模型驱动的应用程序的框架,它提供了一套工具、组件和接口,可以简化构建高级语言模型应用程序的过程。利用LangChain可以使应用程序具备两个能力: 上下文感知 将语言模型与上下文(提示…...
Nginx 核心配置详解与性能优化最佳实践
1.什么是 Nginx? Nginx 是一个高性能的 Web 服务器和反向代理服务器。它轻量、高效,被广泛用于现代 Web 开发中。 2.为什么前端需要了解 Nginx? ★ 了解 本地开发:可以模拟生产环境 部署前端项目:作为静态文件服务器…...
机器学习的一百个概念(7)独热编码
前言 本文隶属于专栏《机器学习的一百个概念》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和参考文献请见[《机器学习的一百个概念》 ima 知识库 知识库广场搜索&…...
用LLama factory时报类似Process 2504721 got signal: 1的解决方法
之前用nohup来远程跑LLama factory微调脚本,是没有问题的,但今天发现运行类似下面这个命令时, nohup llamafactory-cli train examples/train_qlora/qwen_lora.yaml 只要一关闭ssh session,就会终止训练,报类似&…...
解决 Git 通过 SSH 克隆仓库时自动转换为 HTTPS 的问题
解决 Git 通过 SSH 克隆仓库时自动转换为 HTTPS 的问题 在使用 Git 通过 SSH 协议克隆私有仓库时,如果遇到类似以下错误: fatal: unable to access https://itlab.stack.net:stack.git/: Could not resolve host: gitlab.stack这通常是因为 Git 配置错…...
从实用的角度聊聊Linux下文本编辑器VIM
本文从实用的角度聊聊Vim的常用命令。何为实用?我举个不实用的例子大家就明白了,用vim写代码。;) “vim是从 vi 发展出来的一个文本编辑器。代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用,和Emacs并列成…...
多电机显示并排序
多电机显示并排序 要实现根据后端传递过来的驱动电机数据的数量来显示不同数量的数据列表,我们可以使用 Vue 的 v-for 指令来遍历 driveMotorData 数组,并为每个驱动电机生成一个数据列表。这样,无论后端传来多少个驱动电机的数据࿰…...
佳能imageRUNNER 2206N基本参数及管理员密码
基本参数: 产品类型 激光数码复合机 颜色类型 黑白 涵盖功能 复印/打印/扫描 速度类型 低速 最大原稿尺寸 A3 复印/打印方式 激光静电转印方式 感光材料 OPC 显影系统 干式单组分显影 定影…...
社交类 APP 设计:打造高用户粘性的界面
在当今数字化时代,社交类APP已成为人们日常生活中不可或缺的一部分。然而,随着市场竞争的加剧,如何通过设计提升用户粘性成为社交类APP成功的关键。本文将从设计的关键要素、用户界面优化、功能创新、个性化体验以及持续优化等方面࿰…...
数据编排与Dagster:解锁现代数据管理的核心工具
在数据驱动的时代,如何高效管理复杂的数据管道、确保数据质量并实现团队协作?本文深入探讨数据编排的核心概念,解析其与传统编排器的差异,并聚焦开源工具Dagster如何以“资产为中心”的理念革新数据开发流程,助力企业构…...
网络安全中的“后门”:概念、类型、作用与攻防技术
目录 什么是后门? 后门的常见类型 2.1 按植入方式分类 2.2 按功能分类 后门在安全测试中的作用 后门的玩法与免杀技术 4.1 常见后门技术 4.2 如何实现免杀(Bypass AV) 如何检测和防御后门? 总结 1. 什么是后门ÿ…...
AI | 大模型入门介绍
以下是关于AI大模型中蒸馏、量化、MoE和MHA技术的介绍: 1. 模型蒸馏(Model Distillation) • 定义:模型蒸馏是一种将大型复杂模型(教师模型)的知识转移到小型简单模型(学生模型)的技…...
Jmeter的压测使用
Jmeter基础功能回顾 一、创建Jmeter脚本 1、录制新建 (1)适用群体:初学者 2、手动创建 (1)需要了解Jmeter的常用组件 元件:多个类似功能组件的容器(类似于类) 各元件作用 组件…...
kubernetes》》k8s》》Deployment》》ClusterIP、LoadBalancer、Ingress 内部访问、外边访问
Nginx部署 K8s 集群内外访问服务的方式 节点 Kubernetes 集群中的服务器(指单台) 集群 Kubernetes 管理的一组服务器的集合 边界路由器 为局域网和Internet路由数据包的路由器,执行防火墙保护局域网络 集群网络 遵循Kubernetes网络模型实现集…...
Transformer 通关秘籍8:词向量如何表示近义词?
上一节已经完成了 token 到词向量的转换。那么,使用转换后的词嵌入向量便可以表示 token 之间的语义了吗?便可以表示两个单词是否是近义词,是否是反义词了吗? 是的。 接下来先通过一个例子,来直观地理解一下词嵌入向…...
Vue + Scss项目中实现自定义颜色主题的动态切换
当时面试的时候遇到面试官问的一个问题如何实现自定义颜色主题切换,当时我做的只是elementUIPlus提供的暗黑和默认主题切换 theme.scss // 增加自定义主题类型 $themes: (light: (/* 原有配置保持不变 */),dark: (/* 原有配置保持不变 */),custom: () // 空映射…...
搭建qemu环境
1.安装qemu apt install qemu-system2.编译内核 设置gcc软链接sudo ln -s arm-linux-gnueabihf-gcc arm-linux-gccsudo ln -s arm-linux-gnueabihf-ld arm-linux-ldsudo ln -s arm-linux-gnueabihf-nm arm-linux-nmsudo ln -s arm-linux-gnueabihf-objcopy arm-linux-objc…...
【MVC简介-产生原因、演变历史、核心思想、组成部分、使用场景】
MVC简介 产生原因: MVC(Model-View-Controller)模式诞生于20世纪70年代,由Trygve Reenskaug在施乐帕克研究中心(Xerox PARC)为Smalltalk语言设计,目的是解决图形用户界面(GUI&…...
基于NebulaGraph构建省市区乡镇街道知识图谱(二)
上次我们有讲到构建知识图谱,但是在实际使用的时候会发现某些乡镇街道丢失的问题,因为VID必须全局唯一,覆盖导致原因,另外在全国大批量导入时速度非常慢,为此,我们重新优化表结构与导入语法。 1. 表及索引…...
论文浅尝 | Interactive-KBQA:基于大语言模型的多轮交互KBQA(ACL2024)
转载至:何骏昊 开放知识图谱 原文地址:论文浅尝 | Interactive-KBQA:基于大语言模型的多轮交互KBQA(ACL2024) 笔记整理:何骏昊,东南大学硕士,研究方向为语义解析 论文链接ÿ…...
线性规划工具推荐篇 开源+商用 按需取用
一、开源免费工具 1. GLPK (GNU Linear Programming Kit) 特点: 支持线性规划(LP)、混合整数规划(MIP)使用MathProg语言建模,可通过glpsol命令行求解适合中小规模问题,性能低于商业求解器 适用…...
