多线程(七):单例模式指令重排序

目录

1. 单例模式

1.1 饿汉模式

1.2 懒汉模式

2. 懒汉模式下的问题

2.1 线程安全问题

2.2 如何解决 --- 加锁

 2.3 加锁引入的新问题 --- 性能问题

2.4 指令重排序问题

2.4.1 指令重排序

2.4.2 指令重排序引发的问题


1. 单例模式

单例模式, 是设计模式中最典型的一种模式, 是一种比较简单的模式, 同时也是面试中最容易被问到的模式.

什么是设计模式呢?

我们可以把设计模式模式理解为棋谱, 大佬们将棋局中技巧记录下来, 而我们只要根据棋谱来下棋, 结果就不会太差~ 

而设计模式就是我们程序的"棋谱", 大佬们把一些典型的问题整理出来, 并且告诉我们针对这些问题, 代码该如何写, 给出了一些指导和建议.

而我们程序员根据设计模式来写代码, 不管水平高低, 写出来的代码也都不会太差~

而单例模式, 就是设计模式的一种.

在单例模式中, 强制要求某个类, 在一个程序(进程)中, 只能有唯一一个实例(不允许创建多个实例, 不允许 new 多次).

举两个例子:

  1. 在学习 MySQL JDBC 时, 编写 JBBC 的第一步的就是要创建 DataSource, DataSource描述了数据库服务器的信息(URL, user, password). 由于数据库只有一份, 即使创建多个这样的对象也没有意义(即使创建了多个对象, 存的也都是一样的信息). 所以 DataSource 是非常适合于用作单例的.
  2. 还比如在实际开发中, 会通过类组织大量的数据, 而这个类的实例就可能管理几百G的内存数据, 而一个服务器的内存容量也可能就几百G, 所以从开销来说, 也必须只能有一个实例.

而单例模式, 就是强制要求某个类, 在程序中, 只能有一个实例.

而这样的规定, 并不是口头上的一个"君子协定", 而是通过程序 / 代码技巧 / 机器, 来强制要求只能用一个实例.(如果菜鸡程序员 new 了两个对象, 直接编译失败~)

单例模式式具体的实现方式有很多种(通过编程技巧). 最常见的是 "饿汉模式" 和 "懒汉模式" 两种

1.1 饿汉模式

饿汉模式, 是单例模式的一种.

顾名思义, "饿汉", 就是迫切的意思, 通过创建 static 修饰的实例作为成员, 使得实例在类加载时就被创建. 

类加载在程序一启动时就被触发, 所以静态的成员的初始化也是在类加载的阶段完成的.

同时, 我们也要确保类的实例只能被创建一次, 所以可以通过构造方法私有化的形式完成, 这样一来, 在类外面进行 new 操作, 就会编译报错.

/*** 饿汉模式*/
class Singleton {//在类加载时就对实例进行初始化private static Singleton instance = new Singleton();//构造方法私有化 -> 防止类外的 new 操作private Singleton() {}//获取实例public static Singleton getInstance() {return instance;}
}

1.2 懒汉模式

懒汉模式, 也是单例模式的一种.

"懒"和"饿"是相对的一组概念. "饿", 是尽早创建实例; 而"懒", 是尽量晚的去创建实例(延迟创建, 甚至不创建).

在实际生活中, "懒"意味着拖拖拉拉, 不勤快, 不靠谱~

但是在计算机中, "懒"是一个 褒义词~~

举个例子:

当我们打开一个很大的文件时(千万字的小说), 编辑器可以有两个选择:

  1. 加载所有内容到内存中后, 再显示到你的屏幕.
  2. 只加载一部分内容, 随着用户翻页而再加载其他内容.

很明显, 计算机肯定会选择第二个方式来加载数据, 如果采用第一个方式肯定会占用大量内存空间, 造成设备卡顿. 

所以, 在懒汉模式下, 这一个实例创建的时机, 是在我们第一次使用的时候的才创建, 而不是程序刚开始启动的时候.

/*** 懒汉模式*/
class SingletonLazy {private static SingletonLazy instance = null;private SingletonLazy() {}public static SingletonLazy getInstance() {if(instance == null) {instance = new SingletonLazy();}return instance;}
}

饿汉 / 懒汉 的模式是存在缺陷的, 比如可以通过 反射 来创建类的实例.

但是, 反射本就是一个"非常规"的编程手段, 所以在开发中, 也不推荐使用反射.


2. 懒汉模式下的问题

2.1 线程安全问题

在上文, 我们分别编写了 饿汉 / 懒汉 单例模式的代码, 那这两份代码是否是线程安全的呢???

换句话说, 两个版本的getInstance方法, 在多线程环境下调用, 是否会出现 bug 呢???

  • 在饿汉模式下, 由于实例在类加载时就被创建好了, getInstance方法只是返回实例, 并非涉及修改, 所以必然是线程安全的~
  • 而再懒汉模式下, getInstance方法出现了赋值 " = " 操作, 故涉及到了数据的修改, 故可能存在线程安全问题.

到这里, 相信大家心里有了疑问 : "虽然 = 是修改操作, 但是它是原子的啊 , 不是说原子的操作是线程安全的吗???"

是的, 没错, = 虽然是原子的, 但是 = 和其上面的 if 搭配起来, 就并非原子的了~ 再加上操作系统的随机调度, 可能就会导致线程安全问题.

我们来看以下两个线程这样的调度情况:

调度过程如下 : 

  1. t1 先进入 if , 此时还没有进行 new 操作,
  2. t1 被调度走, t2 被调度来, 
  3. t2 仍然满足 if 的条件判断, 
  4. t1 再调度来, 进行 new 操作, 返回实例
  5. t1 被调度走, t2 调度来,
  6. t2 进行 new 操作, 返回实例

虽然, 随着 t2 的 new 操作返回, t1 new 的对象覆盖, 也会被 GC 回收, 但是, 在 new 的过程中, 可能要把大量的数据从硬盘加载到内存中, 这将是双倍的开销, 将大幅度拉低程序性能.

2.2 如何解决 --- 加锁

对于线程安全问题, 加锁是一个常规手段~~

我们上文说到, 虽然 = 是原子的, 但是 = 和 if 组合起来就并非原子的了, 那我们就可以使用 synchronized 将这些操作打包成原子的.

注意: 一定要把赋值操作和 if 一起打包放在 synchronized 中, 不能只放赋值操作. 我们希望的是将 条件和修改 一起打包成原子操作.

加上锁后, 后执行的线程就会在加锁的位置阻塞, 直到前一个线程 new 操作后才解除阻塞状态, 而此时的 instance 不再为 null , 后执行的线程也就不能进入 if 中, 不会再进行 new 操作.

我们同样也可以通过给方法加锁的方式来解决(相当于给类对象 SingletonLazy.class 加锁):

综上, 通过将 条件和修改 加锁打包成原子, 解决了线程安全问题.

 2.3 加锁引入的新问题 --- 性能问题

将 条件和修改 通过加锁打包成原子后, 解决了线程安全问题, 但是又引入了一个新问题 : 性能问题.

我们上文所说的线程安全问题, 是在 instance 还没有创建的情况下.

但是当实例已经被创建好后, getInstance方法的作用就只是单独的读操作(只需返回实例即可), 而读操作, 不涉及线程安全问题.

但是, 我们加上锁后, 每次的读操作都会进行加锁操作, 在多线程下意味着线程间会发生阻塞等待, 从而影响程序的执行效率.

有句古话说得好, "温饱思淫欲" , 现在程序已经解决了线程安全问题(温饱问题解决了), 但是现在我们想要他跑的更快, 效率更高(思淫欲)~~

那么该如何做呢? 

--- 按需加锁, 当涉及到线程安全问题的时候, 就加锁; 当不涉及线程安全问题的时候, 就不用加锁.

锁的外面再加上一个 if 判断即可:

在以往的单线程环境下, 连续的两个相同的 if 是没有意义的. 

但是在多线程环境下, 程序中有多个执行流, 很可能在两个 if 间, 就有其他线程把值给修改了, 从而导致两次的 if 结果不同.

并且若中间有锁, 一旦阻塞, 阻塞的时间间隔, 对于计算机来说就是"沧海桑田". 这中间变量的变化, 都是不得而知的, 所以要再加一次 if 的条件判断.

这里的代码上的两个 if , 作用也是完全不一样的:

  1. 最外层的 if 是判断是否需要加锁
  2. 里面的 if 是判断是否需要 new 对象

故, 在最外层加上 if 后, 解决了性能问题~

2.4 指令重排序问题

到目前为止, 通过对上述代码的改进, 已经解决了线程安全问题和性能问题.

但是, 上述代码仍旧存在由 指令重排序 而引起的问题~

2.4.1 指令重排序

指令重排序和之前提到的内存可见性问题一样, 都是编译器优化的体现形式.

指令重排序: 编译器会在原有代码逻辑不变的情况下, 对代码的执行的先后顺序进行调整, 以达到提升性能的效果.

举个例子:

放假后在家, 你妈给了你一个清单, 叫你去超市买清单上的蔬菜, 清单上的蔬菜如下:

  1. 西红柿
  2. 土豆
  3. 茄子
  4. 白菜

到了超市后, 你会严格的按照清单上的顺序去买菜吗? 

并不是, 你会根据菜和你的位置, 来决定先买哪个后买哪个, 以至可以走"最小路径".

所以, 编译器也是一样, 在逻辑不变的大前提下, 会调整代码的执行顺序来提高性能.

但是在多线程的环境下, 编译器的调整就可能出现错误, 导致指令重排序问题的发生.

2.4.2 指令重排序引发的问题

比如, 在上述代码中对 instance 的 new 操作(即创建实例的过程), 分为以下三步:

  1. 申请内存空间
  2. 在空间上构造对象(完成初始化)
  3. 将内存空间的首地址, 赋值给引用变量

正常来说, 这三步是按照 1 2 3 的步骤执行的, 但是经过指令重排序,  可能成为 1 3 2 这样的顺序.

在单线程的环境下, 这两个顺序都无所谓, 最后得到的都是一个囫囵个的完整的对象.

但是在多线程下, 就会出现问题了 :

如上图所示, 若经过指令重排序, 创建实例的过程被修改为 1 3 2. 一个线程在进行 new 时, 只进行了 1 3 步骤(还没有对实例进行初始化), 此时该线程被切走, 另一个线程执行时, 发现 instance 不为空, 直接返回了对象, 但是这个对象却还没有初始化, 那么后续使用这个对象就会出 bug 了~~

对于指令重排序问题, 依然需要用到 volatile 关键字, 我们可以使用 volatile 关键字来修饰 instance 来避免指令重排序带来的问题.

所以, volatile 关键字的功能有两点:

  1. 确保每次的读取操作, 都是从内存读取
  2. 被 volatile 修饰的变量, 关于该变量读取和修改操作, 不会触发重排序.

并且, 编译器优化这个事情, 是非常复杂的, 所以我们也不能确保内存可见性问题是否存在, 所以为了稳妥起见, 从根本上杜绝内存可见性问题, 我们也可以给 instance 加上 volatile.

综上, volatile 禁止了指令重排序, 保证了内存可见性.


END

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

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

相关文章

Vision China 2024 | 移远通信以一体化的AI训练及部署能力,引领3C电子制造智能升级

10月14日,由机器视觉产业联盟(CMVU)主办的中国机器视觉展(Vision China)在深圳国际会展中心盛大开幕。作为全球领先的物联网整体解决方案供应商,移远通信应邀参加展会首日举办的“智造引领数质并进”3C电子制造自动化与数字化论坛。 论坛上,移…

PostgreSQL学习笔记:PostgreSQL vs MySQL

PostgreSQL 和 MySQL 都是广泛使用的关系型数据库管理系统,它们有以下一些对比: 一、功能特性 1. 数据类型支持 PostgreSQL:支持丰富的数据类型,包括数组、JSON、JSONB、范围类型、几何类型等。对于复杂数据结构的存储和处理非…

rancher安装并快速部署k8s 管理集群工具

主机准备 准备4台主机 3台用于k8s集群 ,1台用于rancher 每台服务器新增配置文件 vi etc/sysctl.confnet.ipv4.ip_forward 1 刷新生效 sysctl –p 安装docker 安装的时候可以去github上检索rancher看看最新版本适配那个版本的docker,这里安装23.0.1…

酸碱PH值与浓度关系

1. 硫酸百分比浓度是指溶液中硫酸的质量占溶液总质量的百分比。‌ 例如,如果100克溶液中含有98克的硫酸,那么硫酸的百分比浓度为98% 2. 1mol/L硫酸对应百分比浓度多少?答:硫酸的质量分数98,1mol/L硫酸98g/L9.8%的硫酸…

RNN,LSTM,GRU的区别和联系? RNN的梯度消失问题?如何解决?

RNN,LSTM,GRU的区别和联系? RNN(Recurrent Neural Network)、LSTM(Long Short-Term Memory)和GRU(Gated Recurrent Unit)都是用于处理序列数据的神经网络模型,它们之间…

动态规划:17.简单多状态 dp 问题_买卖股票的最佳时机III_C++

题目链接: 一、题目解析 题目:123. 买卖股票的最佳时机 III - 力扣(LeetCode) 解析: 拿示例1举例: 我们可以如图所示买入卖出股票,以求得最大利润,并且交易次数不超过2次 拿示…

二百六十九、Kettle——ClickHouse清洗ODS层原始数据增量导入到DWD层表中

一、目的 清洗ClickHouse的ODS层原始数据,增量导入到DWD层表中 二、实施步骤 2.1 newtime select( select create_time from hurys_jw.dwd_statistics order by create_time desc limit 1) as create_time 2.2 替换NULL值 2.3 clickhouse输入 2.4 字段选择 2.5 …

Git的原理和使用(三)

1. 分支管理 1.1 合并模式 1.1.1 fast forward模式 git log --graph --abbrev-commit 1.1.2 no-ff模式 合并出现问题后需要进行手动修改: 如下图所示: 1.1.3 不使用no-ff模式 git merge --no-ff -m "merge dev2" dev2 1.2 分⽀策略 在实际开…

多IP访问多网段实验

文章目录 多IP访问多网段实验 多IP访问多网段实验 在当前主机配置多个IP地址,实现多IP访问多网段,记录所有命令及含义 1,环境搭建: [rootlocalhost ~]# mount /dev/sr1 /mnt # 设置ISO虚拟镜像文件文件挂载点,将…

数据分析和可视化python库orange简单使用方法

Orange 是一个基于 Python 的数据挖掘和机器学习库,它提供了一系列可视化工具和算法,用于数据分析、机器学习和数据可视化等任务。 一、主要特点 可视化界面:Orange 提供了直观的可视化界面,使得用户可以通过拖放操作构建数据分…

【python爬虫实战】爬取全年天气数据并做数据可视化分析!附源码

由于篇幅限制,无法展示完整代码,需要的朋友可在下方获取!100%免费。 一、主题式网络爬虫设计方案 1. 主题式网络爬虫名称:天气预报爬取数据与可视化数据 2. 主题式网络爬虫爬取的内容与数据特征分析: - 爬取内容&am…

算法(四)前缀和

前缀和也是一个重要的算法,一般用来快速求静态数组的某一连续区间内所有数的和,效率很高,但不支持修改操作。分为一维前缀和、二维前缀和。 重要的前言! 不要死记模板,具体题目可能是前缀和、前缀乘积、后缀和、后缀乘…

已解决:ModuleNotFoundError: No module named ‘pip‘

[已解决] ModuleNotFoundError: No module named ‘pip‘ 文章目录 写在前面问题描述报错原因分析 解决思路解决办法1. 手动安装或升级 pip2. 使用 get-pip.py 脚本3. 检查环境变量配置4. 重新安装 Python 并确保添加到 PATH5. 在虚拟环境中安装 pip6. 使用 conda 安装 pip&…

无人机电机故障率骤降:创新设计与六西格玛方法论双赢

项目背景 TBR-100是消费级无人机头部企业推出的主打消费级无人机,凭借其出色的续航能力和卓越的操控性,在市场上获得了广泛认可。在产品运行过程,用户反馈电机故障率偏高,尤其是在飞行一段时间后出现电机过热、损坏以及运行不稳定…

《深度学习》dlib 人脸应用实例 仿射变换 换脸术

目录 一、仿射变换 1、什么是仿射变换 2、原理 3、图像的仿射变换 1)图像的几何变换主要包括 2)图像的几何变换主要分为 1、刚性变换: 2、仿射变换 3、透视变换 3)常见仿射变换 二、案例实现 1、定义关键点索引 2、定…

OpenHarmony 入门——ArkUI 自定义组件内同步的装饰器@State小结(二)

文章大纲 引言一、组件内状态装饰器State1、初始化2、使用规则3、变量的传递/访问规则说明4、支持的观察变化的场景5、State 变量的值初始化和更新机制6、State支持联合类型实例 引言 前一篇文章OpenHarmony 入门——ArkUI 自定义组件之间的状态装饰器小结(一&…

100多种【基于YOLOv8/v10/v11的目标检测系统】目录(python+pyside6界面+系统源码+可训练的数据集+也完成的训练模型)

待更新(持续更新),早关注,不迷路............................................................................... 基于YOLOv8的车辆行人实时检测系统基于YOLOv10的车辆行人实时检测系统基于YOLOv11的车辆行人实时检测系统基于YOLOv8的农…

如何在UE5中创建加载屏幕(开场动画)?

第一步: 首先在虚幻商城安装好Async Loading Screen,并且在项目的插件中勾选好。 第二步: 确保准备好所需要的素材: 1)开头的动画视频 2)关卡加载图片 3)准备至少两个关卡 第三步&#xff1a…

PythonExcel批量pingIP地址

问题: 作为一个电气工程师(PLC),当设备掉线的时候,需要用ping工具来检查网线物理层是否可靠连接,当项目体量过大时,就不能一个手动输入命令了。 解决方案一: 使用CMD命令 for /L %…

二百六十八、Kettle——同步ClickHouse清洗数据到Hive的DWD层静态分区表中(每天一次)

一、目的 实时数仓用的是ClickHouse,为了避免Hive还要清洗数据,因此就直接把ClickHouse中清洗数据同步到Hive中就行 二、所需工具 ClickHouse:clickhouse-client-21.9.5.16 Kettle:kettle9.2 Hadoop:hadoop-3.1.3…