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

目录

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ArcGIS微课1000例】0125:ArcGIS矢量化无法自动完成面解决方案

文章目录 一、坐标系统问题二、正确使用自动完成面工具一、坐标系统问题 1. 数据库坐标系 arcgis矢量化的过程中,无法自动完成面,可能是因为图层要素没有坐标系造成的。双击数据库打开数据库属性,可以查看当前数据框的坐标系。 2. 图层坐标系 双击图层,打开图层属性,切…

Safari 中 filter: blur() 高斯模糊引发的性能问题及解决方案

目录 引言问题背景:filter: blur() 引发的问题产生问题的原因分析解决方案:开启硬件加速实际应用示例性能优化建议常见的调试工具与分析方法 引言 在前端开发中,CSS滤镜(如filter: blur())的广泛使用为页面带来了各种…

使用query-string库出现错误Module parse failed: Unexpected token

环境 node v12query-string 9.1.0 报错信息 Failed to compile../node_modules/query-string/base.js 350:14 Module parse failed: Unexpected token (350:14) File was processed with these loaders:* ./node_modules/babel-loader/lib/index.js You may need an additio…

正则表达式和通配符

文章目录 正则表达式和通配符的区别正则表达式(Regex)通配符(Wildcards)总结 正则表达式的概念正则表达式的由来为什么要使用正则表达式 正则表达式的语法组成修饰符元字符\f\b\B 在Linux中的基础正则和扩展正则基础正则(BRE)^$.*…

【南方科技大学】CS315 Computer Security 【Lab6 IoT Security and Wireless Exploitation】

目录 Introduction (Part 1: OS Security for IoT )Software RequirementsStarting the Lab 6 Virtual MachineSetting up the Zephyr Development EnvironmentDownload the Zephyr Source CodeInstalling Requirements and DependenciesSetting the Project’s Environment Va…

《a16z : 2024 年加密货币现状报告》解析

加密社 原文链接:State of Crypto 2024 - a16z crypto译者:AI翻译官,校对:翻译小组 当我们两年前第一次发布年度加密状态报告的时候,情况跟现在很不一样。那时候,加密货币还没成为政策制定者关心的大事。 比…

Ubuntu 安装 npm

1. 升级apt sudo apt-get update 2. 安装nodejs sudo apt install nodejs 3. 安装npm sudo apt-get install npm 4. 查看版本 node -v npm -v 完成安装!

记一次AWS服务器扩容

1、首先通过下列命令列出设备详情,可以看到红色框起来的部分有160G,需要把新增的20G扩容到根目录(139.9)上 lsblk查看文件系统 df -h2.执行sudo growpart /dev/xvda 1即可把20G的空间扩容到根目录上 扩容成功 但是可以看到并未生效 3.列出文件系统格…

ue5实现数字滚动增长

方法1 https://www.bilibili.com/video/BV1h14y197D1/?spm_id_from333.999.0.0 b站教程 重写loop节点 方法二 写在eventtick里

NVR小程序接入平台/设备EasyNVR多品牌NVR管理工具/设备的多维拓展与灵活应用

在数字化安防时代,NVR批量管理软件/平台EasyNVR作为一种先进的视频监控系统设备,正逐步成为各个领域监控解决方案的首选。NVR批量管理软件/平台EasyNVR作为一款基于端-边-云一体化架构的国标视频融合云平台,凭借其部署简单轻量、功能多样、兼…

什么是DICOM文件?——认识DICOM:医学影像与信息管理的标准化利器

目录 引言 什么是DICOM? DICOM的组成 DICOM的功能 DICOM的应用 DICOM的种类 DICOM的生成过程 DICOM的发展 总结 引言 在现代医学中,影像处理和管理是不可或缺的一环。从MRI、CT、X射线到超声波,医学影像为诊断和治疗提供了丰富的信息…

iOS 本地存储地址(位置)

前言: UserDefaults 存在沙盒的 Library --> Preferences--> .plist文件 CoreData 存在沙盒的 Library --> Application Support--> xx.sqlite 一个小型数据库里 (注:Application Support 这个文件夹已开始是没有的,只有当你写了存储代码,运行之后,目录里才会出…

django个人博客管理系统-计算机毕业设计源码27633

目 录 1 绪论 1.1 研究背景和意义 1.2国内外研究现状 1.3论文结构与章节安排 2 系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 操作可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析 2.3 系统用例分析 2.4 系统流程…

任务看板是什么?如何选择合适的任务看板工具?

一、任务看板是什么? 任务看板是一种可视化的项目管理工具,它通常以板状的形式呈现,将任务以卡片的形式展示在不同的列中,每一列代表任务的不同状态。例如,待办事项、进行中、已完成等。任务看板能够帮助团队成员清晰…

使用 Flask 实现简单的登录注册功能

目录 1. 引言 2. 环境准备 3. 数据库设置 4. Flask 应用基本配置 5. 实现用户注册 6. 实现用户登录 7. 路由配置 8. 创建前端页面 9. 结论 1. 引言 在这篇文章中,我们将使用 Flask 框架创建一个简单的登录和注册系统。Flask 是一个轻量级的 Python Web 框架…

合合信息亮相2024中国模式识别与计算机视觉大会,用AI构建图像内容安全防线

近日,第七届中国模式识别与计算机视觉大会(简称“PRCV 2024”)在乌鲁木齐举办。大会由中国自动化学会(CAA)、中国图象图形学学会(CSIG)、中国人工智能学会(CAAI)和中国计…

pytorh学习笔记——cifar10(六)MobileNet V1网络结构

基础知识储备: 一、深度可分离卷积(Depthwise Separable Convolution) MobileNet的核心是深度可分离卷积(Depthwise Separable Convolution),深度可分离卷积是卷积神经网络(CNN&#xf…

IDM下载器 (Internet Download Manager) v6.42.2 中文免激活绿色版

Internet Download Manager (IDM下载器) 是一款先进的下载工具,可以提升您的下载速度高达5倍,支持续传,IDM可以让用户自动下载某些类型的文件,它可将文件划分为多个下载点以更快速度下载,并列出最近的下载,方便访问文件。相对于其…

Web刷题日记1---清风

[GDOUCTF 2023]EZ WEB 题目网站在NSSCTF 这个题目有一个新的知识点,对于我来说比较的少见吧,第一次遇见。em...是什么呢?后面再说 进入靶场,比较突兀,点了这个button后,提示flag在附近 查看源码,有提示…

C#从零开始学习(用户界面)(unity Lab4)

这是书本中第四个unity Lab 在这次实验中,将学习如何搭建一个开始界面 分数系统 点击球,会增加分数 public void ClickOnBall(){Score;}在OneBallBehaviour类添加下列方法 void OnMouseDown(){GameController controller Camera.main.GetComponent<GameController>();…