多线程代码案例 - 1

news2025/4/26 10:43:44

目录

单例模式

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,也就杜绝了上述问题了!

完!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2327013.html

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

相关文章

开发体育赛事直播系统主播认证功能技术实现方案

该体育直播系统系统由东莞梦幻网络科技开发&#xff0c;使用 ThinkPHP 作为后端&#xff0c;Vue.js 作为 PC/H5 端框架&#xff0c;Java 和 Objective-C 分别用于安卓和 iOS 开发。 1、前端实现 (Vue.js) <template><div class"anchor-certification">…

国产三维CAD「皇冠CAD」在汽车零部件领域建模教程:刹车片

本教程深度融合三维皇冠CAD&#xff08;CrownCAD&#xff09;的MBD&#xff08;Model-Based Definition&#xff09;设计理念&#xff0c;通过参数化建模、智能约束管理、动态装配验证等功能&#xff0c;实现数据驱动设计&#xff0c;精准解决了汽车制动系统中精密制动组件的设…

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进行编写&#xff0c;通过框架可以提取url中的name与age&#xff0c;这…

大语言模型开发框架——LangChain

什么是LangChain LangChain是一个开发由语言模型驱动的应用程序的框架&#xff0c;它提供了一套工具、组件和接口&#xff0c;可以简化构建高级语言模型应用程序的过程。利用LangChain可以使应用程序具备两个能力&#xff1a; 上下文感知 将语言模型与上下文&#xff08;提示…

机器学习的一百个概念(7)独热编码

前言 本文隶属于专栏《机器学习的一百个概念》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和参考文献请见[《机器学习的一百个概念》 ima 知识库 知识库广场搜索&…

从实用的角度聊聊Linux下文本编辑器VIM

本文从实用的角度聊聊Vim的常用命令。何为实用&#xff1f;我举个不实用的例子大家就明白了&#xff0c;用vim写代码。;) “vim是从 vi 发展出来的一个文本编辑器。代码补全、编译及错误跳转等方便编程的功能特别丰富&#xff0c;在程序员中被广泛使用&#xff0c;和Emacs并列成…

佳能imageRUNNER 2206N基本参数及管理员密码

基本参数&#xff1a; 产品类型 激光数码复合机 颜色类型 黑白 涵盖功能 复印/打印/扫描 速度类型 低速 最大原稿尺寸 A3 复印/打印方式 激光静电转印方式 感光材料 OPC 显影系统 干式单组分显影 定影…

社交类 APP 设计:打造高用户粘性的界面

在当今数字化时代&#xff0c;社交类APP已成为人们日常生活中不可或缺的一部分。然而&#xff0c;随着市场竞争的加剧&#xff0c;如何通过设计提升用户粘性成为社交类APP成功的关键。本文将从设计的关键要素、用户界面优化、功能创新、个性化体验以及持续优化等方面&#xff0…

数据编排与Dagster:解锁现代数据管理的核心工具

在数据驱动的时代&#xff0c;如何高效管理复杂的数据管道、确保数据质量并实现团队协作&#xff1f;本文深入探讨数据编排的核心概念&#xff0c;解析其与传统编排器的差异&#xff0c;并聚焦开源工具Dagster如何以“资产为中心”的理念革新数据开发流程&#xff0c;助力企业构…

Jmeter的压测使用

Jmeter基础功能回顾 一、创建Jmeter脚本 1、录制新建 &#xff08;1&#xff09;适用群体&#xff1a;初学者 2、手动创建 &#xff08;1&#xff09;需要了解Jmeter的常用组件 元件&#xff1a;多个类似功能组件的容器&#xff08;类似于类&#xff09; 各元件作用 组件…

kubernetes》》k8s》》Deployment》》ClusterIP、LoadBalancer、Ingress 内部访问、外边访问

Nginx部署 K8s 集群内外访问服务的方式 节点 Kubernetes 集群中的服务器&#xff08;指单台&#xff09; 集群 Kubernetes 管理的一组服务器的集合 边界路由器 为局域网和Internet路由数据包的路由器&#xff0c;执行防火墙保护局域网络 集群网络 遵循Kubernetes网络模型实现集…

Transformer 通关秘籍8:词向量如何表示近义词?

上一节已经完成了 token 到词向量的转换。那么&#xff0c;使用转换后的词嵌入向量便可以表示 token 之间的语义了吗&#xff1f;便可以表示两个单词是否是近义词&#xff0c;是否是反义词了吗&#xff1f; 是的。 接下来先通过一个例子&#xff0c;来直观地理解一下词嵌入向…

【MVC简介-产生原因、演变历史、核心思想、组成部分、使用场景】

MVC简介 产生原因&#xff1a; MVC&#xff08;Model-View-Controller&#xff09;模式诞生于20世纪70年代&#xff0c;由Trygve Reenskaug在施乐帕克研究中心&#xff08;Xerox PARC&#xff09;为Smalltalk语言设计&#xff0c;目的是解决图形用户界面&#xff08;GUI&…

基于NebulaGraph构建省市区乡镇街道知识图谱(二)

上次我们有讲到构建知识图谱&#xff0c;但是在实际使用的时候会发现某些乡镇街道丢失的问题&#xff0c;因为VID必须全局唯一&#xff0c;覆盖导致原因&#xff0c;另外在全国大批量导入时速度非常慢&#xff0c;为此&#xff0c;我们重新优化表结构与导入语法。 1. 表及索引…

论文浅尝 | Interactive-KBQA:基于大语言模型的多轮交互KBQA(ACL2024)

转载至&#xff1a;何骏昊 开放知识图谱 原文地址&#xff1a;论文浅尝 | Interactive-KBQA&#xff1a;基于大语言模型的多轮交互KBQA&#xff08;ACL2024&#xff09; 笔记整理&#xff1a;何骏昊&#xff0c;东南大学硕士&#xff0c;研究方向为语义解析 论文链接&#xff…

linux -- php 扩展之xlswriter

xlswriter - PHP 最强性能 Excel 扩展 linux 安装 完整编译安装步骤 ## 下载wget https://pecl.php.net/get/xlswriter tar -zxvf xlswriter cd xlswriterphpize # 执行配置 ./configure # 编译 make make install ./configure 如果报错&#xff0c;就指定配置路径 …

Dockerfile文件构建镜像Anaconda+Python教程

文章目录 前言Dockerfile 核心模块解析**一、Dockerfile基础镜像选择二、系统基础配置1、时区设置2、镜像源替换 三、系统依赖安装四、复制本地文件五、指定路径六、Anaconda环境配置1、anaconda环境安装2、配置虚拟环境3、创建conda虚拟环境4、启动和安装环境 七、完整dockerf…

本地部署大模型-web界面(ollama + open-webui)

一、安装ollama 二、安装部署open-webui 1、项目运行环境 &#xff08;1&#xff09;配置python环境—官方下载链接 可通过命令行直接更改python镜像源为阿里云镜像源&#xff1a; >pip config set global.index-url http://mirrors.aliyun.com/pypi/simple/也可手动修…

Java虚拟机JVM知识点(已完结)

JVM内存模型 介绍下内存模型 根据JDK8的规范&#xff0c;我们的JVM内存模型可以拆分为&#xff1a;程序计数器、Java虚拟机栈、堆、元空间、本地方法栈&#xff0c;还有一部分叫直接内存&#xff0c;属于操作系统的本地内存&#xff0c;也是可以直接操作的。 详细解释一下 程…

【C++进阶四】vector模拟实现

目录 1.构造函数 (1)无参构造 (2)带参构造函数 (3)用迭代器构造初始化函数 (4)拷贝构造函数 2.operator= 3.operator[] 4.size() 5.capacity() 6.push_back 7.reserve 8.迭代器(vector的原生指针) 9.resize 10.pop_back 11.insert 12.erase 13.memcpy…