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

目录

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.rhkb.cn/news/452777.html

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

相关文章

VMware:Windows主机与CentOS虚拟机文件互传文件共享

注意:本文使用Win10与VMware17pro互传 1. 本地创建文件夹 如下图创建一个文件夹,名字任意 2. 设置本地文件夹权限 右键文件夹 - - 属性 - - 共享 - - 高级共享 - - 权限 - - 如下图全部勾选 - - 应用 - - 确认 3. VMware中设置共享文件夹路径 第一步…

使用Three.js和Force-Directed Graph实现3D知识图谱可视化

先看样式: 在当今信息爆炸的时代,如何有效地组织和展示复杂的知识结构成为一个重要的挑战。3D知识图谱可视化是一种直观、交互性强的方式来呈现知识之间的关系。本文将详细介绍如何使用HTML、JavaScript、Three.js和Force-Directed Graph库来实现一个交互…

【动态规划】【路径问题】下降路经最小和、最小路径和、地下城游戏

4. 下降路径最小和 931. 下降路径最小和 算法原理 确定状态表示 dp[i][j] 表示:到达 [i, j] 位置,最小的下降路径 状态转移方程 dp[i][j] 从 [i-1, j-1] 到达 [i, j] > dp[i-1][j-1] m[i][j]从 [i-1, j] 到达 [i, j] > dp[i-1][j] m[i][j]从 …

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

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

智简魔方业务管理系统v10 好用的IDC业务管理软件

智简魔方业务管理系统v10,您一直在寻找的IDC业务管理软件,基于PHPMYSQL开发的一套小型易于部署的业务管理核心,具有极强的扩展能力,非常方便的安装方式,用户可在5分钟内部署属于自己的业务管理系统,ZJMF-CB…

路由表来源(基于华为模拟器eNSP)

概叙 在交换网络中,若要实现不同网段之间的通信,需要依靠三层设备(路由器、三层交换机等),而路由器只知道其直连网段的路由条目,对于非直连的网段,在默认情况下,路由器是不可达的&a…

Goland 搭建Gin脚手架

一、使用编辑器goland 搭建gin 打开编辑器 新建项目后 点击 create 二、获得Gin框架的代码 命令行安装 go get -u github.com/gin-gonic/gin 如果安装不上,配置一下环境 下载完成 官网git上下载 这样就下载完成了。、 不过这种方法需要设置一下GOPATH 然后再执…

【An】Animate 2024 for【Mac】 An动画设计制作软件 安装教程——保姆级教程

Mac分享吧 文章目录 【An】Animate 2024 Mac版 An动画设计制作软件 安装完成,打开效果Mac电脑【An】Animate 2024 动画设计制作软件——v24.0.4⚠️注意事项:1️⃣:下载软件2️⃣:安装AntiCC组件,步骤见文章或下图3️…

springboot+uinapp基于Android的固定资产借用管理平台

文章目录 前言项目介绍技术介绍功能介绍核心代码数据库参考 系统效果图论文效果图 前言 文章底部名片,获取项目的完整演示视频,免费解答技术疑问 项目介绍 固定资产借用管理平台设计的目的是为用户提供使用申请、故障报修、设备归还、意见反馈等管理方…

嘉立创EDA个人学习笔记2(绘制51单片机核心板)

前言 本篇文章属于嘉立创EDA的学习笔记,来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记,只能做参考,细节方面建议观看视频,肯定受益匪浅。 【教程】零基础入门PCB设计-国一学长带你学立创EDA专业版 全程保姆…

新手入门之Spring Bean

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、初识SpringBootSpringBoot 的主要特点1、自动配置:2、外部化配置:3、嵌入式服务器支持:4、启动器依赖(Start…

大数据新视界 --大数据大厂之数据脱敏技术在大数据中的应用与挑战

💖💖💖亲爱的朋友们,热烈欢迎你们来到 青云交的博客!能与你们在此邂逅,我满心欢喜,深感无比荣幸。在这个瞬息万变的时代,我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

R语言机器学习算法实战系列(十)自适应提升分类算法 (Adaptive Boosting)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍原理步骤教程下载数据加载R包导入数据数据预处理数据描述数据切割调节参数构建模型预测测试数据评估模型模型准确性混淆矩阵模型评估指标ROC CurvePRC Curve特征的重要性保存模型总…

【图解版】力扣第162题:寻找峰值

注意 题目只要求找到一个峰值就可以了。nums[-1]和nums[n]这两个位置是负无穷,也就是说,除了数组的位置之外,其它地方都是负无穷。对于所有有效的 i 都有 nums[i] ! nums[i 1] 方法一 遍历整个数组,找到最高的那个点。时间复杂…

大数据治理:数据时代的挑战与应对

目录 大数据治理:数据时代的挑战与应对 一、大数据治理的概念与内涵 二、大数据治理的重要性 1. 提高数据质量与可用性 2. 确保数据安全与合规 3. 支持数据驱动的决策 4. 提高业务效率与竞争力 三、大数据治理的实施策略 1. 建立健全的数据治理框架 2. 数…

C++STL--------list

文章目录 一、list链表的使用1、迭代器2、头插、头删3、insert任意位置插入4、erase任意位置删除5、push_back 和 pop_back()6、emplace_back尾插7、swap交换链表8、reverse逆置9、merge归并10、unique去重11、remove删除指定的值12、splice把一个链表的结点转移个另一个链表13…

Java入门4——输入输出+实用的函数

在本篇博客,采用代码解释的方法,帮助大家熟悉Java的语法 一、输入和输出 在Java当中,我们一般有这样输入输出: import java.util.Scanner;public class javaSchool {public static void main(String[] args) {Scanner scanner …

【配色网站分享】

个人比较喜欢收藏一些好看的插画、UI设计图和配色,于是有了此篇,推荐一些配色网站,希望能对自己和大家有些帮助。 1.uiGradients 一个主打渐变风网站,还可以直接复制颜色。 左上角的“show all gradients”可以查看一些预设的渐…

Nginx安装于环境配置

1. Nginx-概述 1.1 介绍 ​ Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用ngi…

场景化运营与定制开发链动 2+1 模式 S2B2C 商城小程序的融合

摘要:本文深入探讨了场景化运营的重要性以及其在商业领域的广泛应用。通过分析电梯广告、视频网站和电商产品的场景化运营方式,引入关键词“定制开发链动 21 模式 S2B2C 商城小程序”,阐述了如何将场景化运营理念融入到该小程序的开发与推广中…