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

news2024/11/26 11:02:39

目录

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.coloradmin.cn/o/2222855.html

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

相关文章

CTFHUB技能树之SQL——MySQL结构

开启靶场,打开链接: 先判断一下是哪种类型的SQL注入: 1 and 11# 正常回显 1 and 12# 回显错误,说明是整数型注入 判断一下字段数: 1 order by 2# 正常回显 1 order by 3# 回显错误,说明字段数是2列 知道…

未来医疗:大语言模型如何改变临床实践、研究和教育|文献精析·24-10-23

小罗碎碎念 这篇文章探讨了大型语言模型在医学领域的潜在应用和挑战,并讨论了它们在临床实践、医学研究和医学教育中的未来发展。 姓名单位名称(中文)Jan Clusmann德国德累斯顿工业大学埃尔朗根弗雷斯尼乌斯中心数字化健康研究所Jakob Nikola…

html 轮播图效果

轮播效果: 1、鼠标没有移入到banner,自动轮播 2、鼠标移入:取消自动轮播、移除开始自动轮播 3、点击指示点开始轮播到对应位置 4、点击前一个后一个按钮,轮播到上一个下一个图片 注意 最后一个图片无缝滚动,就是先克隆第一个图片…

动态量化:大模型在端侧CPU快速推理方案

作为一款高性能的推理引擎框架,MNN高度关注Transformer模型在移动端的部署并持续探索优化大模型在端侧的推理方案。本文介绍权重量化的模型在MNN CPU后端的推理方案:动态量化。动态量化指在运行时对浮点型feature map数据进行8bit量化,然后与…

(gersemi) CMake 格式化工具

文章目录 🧮介绍🧮安装🧮使用🗳️模式 modes🗳️样式配置 config ⭐END🌟help🌟交流方式 🧮介绍 BlankSpruce/gersemi: A formatter to make your CMake code the real treasure A f…

Leetcode 最长公共前缀

java solution class Solution {public String longestCommonPrefix(String[] strs) {if(strs null || strs.length 0) {return "";}//用第一个字符串作为模板,利用indexOf()方法匹配,由右至左逐渐缩短第一个字符串的长度String prefix strs[0];for(int i 1; i …

【Java】反射概述与详解

目录 引言 一、概述 二、获取Class对象 三、反射获取构造方法 代码示例: 四、反射获取成员变量 代码示例: 五、反射获取成员方法 代码示例: 结语 引言 Java中的反射(Reflection)是一种强大的机制&#…

热门的四款PDF合并工具大比拼!!!

在现代的数字化办公环境中,PDF文件已经成为了一种重要的文件格式,用于保存和共享各种类型的文档。然而,有时候我们需要将多个PDF文件合并成一个文件,这时候就离不开好用的PDF合并工具了。选择一个好的PDF合并工具是一个长期的投资…

Python基于OpenCV的实时疲劳检测

2.检测方法 1)方法 与用于计算眨眼的传统图像处理方法不同,该方法通常涉及以下几种组合: 1、眼睛定位。 2、阈值找到眼睛的白色。 3、确定眼睛的“白色”区域是否消失了一段时间(表示眨眼)。 相反,眼睛长…

【Power Query】List.Select 筛选列表

List.Select 筛选列表 ——在列表中返回满足条件的元素 List.Select(列表,判断条件) 不是列表的可以转成列表再筛选&#xff0c;例如 Record.ToList 不同场景的判断条件参考写法 (1)单条件筛选 列表中小于50的数字 List.Select({1,99,8,98,5},each _<50) (2)多条件筛…

红黑树(Java数据结构)

前言&#xff1a; 红黑树的学习需要大家对二叉搜索树与AVL树有深刻的理解&#xff0c;如果话没有看过我对二叉搜索树与AVL树的讲解的铁子们可以先看看上一篇文章&#xff1a;二叉搜索树与AVL树(java数据结构)-CSDN博客 红黑树&#xff1a; 什么是红黑树&#xff1f; 红黑树&a…

CenterTrack算法详解

背景&#xff1a; 早期追踪器在缺乏强的低水平线索下&#xff0c;容易失败检测后跟踪的模型依赖于检测器&#xff0c;且需要一个单独的阶段匹配关联策略的时间长 简介&#xff1a; 基于点的跟踪思想&#xff0c;通过预测目标的中心点来进行跟踪&#xff0c;同时实现检测与跟…

LLM在Reranker任务上的最佳实践?A simple experiment report(with code)

知乎&#xff1a;车中草同学(已授权)链接&#xff1a;https://zhuanlan.zhihu.com/p/987727357 引言 在BERT时代&#xff0c;对于Reranker任务&#xff0c;我们使用encoder-only的BERT为基座&#xff0c;拼接query和doc输入到BERT中去&#xff0c;在使用CLS的向量通过一个MLP&a…

身份证识别JAVA+OPENCV+OCR

一、相关的地址 https://github.com/tesseract-ocr/tessdata Releases - OpenCV opencv要装好&#xff0c;我装的是4.5.3的&#xff0c;最新版的没试过。 tessdata就下载了需要用的。好像还有best和fast的版本&#xff0c;我试了一下报错&#xff0c;不知道是不是版本不支持…

华为配置 之 远程管理配置

目录 简介&#xff1a; 知识点&#xff1a; Telnet远程管理 &#xff08;1&#xff09;配置接口IP并确保R1和R2处于同一个网段 &#xff08;2&#xff09;使用password认证模式远程登录 &#xff08;3&#xff09;使用AAA认证模式远程登录 SSH远程管理 &#xff08;1&a…

基于springboot的网上服装商城推荐系统的设计与实现

基于springboot的网上服装商城推荐系统的设计与实现 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;idea 源码获取&#xf…

【deathStarBench】2.安装k8s集群

安装docker 通过以下命令安装docker&#xff1a; sudo yum install docker-ce-26.1.4 docker-ce-cli-26.1.4 containerd.io随后通过查看docker --version&#xff0c;可以确定是否安装的版本一样 启动docker systemctl start docker && systemctl enable docker.se…

《纳瓦尔宝典:财富和幸福指南》读书随笔

最近在罗胖的得到听书中听到一本书&#xff0c;感觉很有启发&#xff0c;书的名字叫《纳瓦尔宝典》&#xff0c;从书名上看给人的感觉应该财富知识类、鸡汤爆棚哪类。纳瓦尔&#xff0c;这个名字之前确实没有听说过&#xff0c;用一句话介绍一下&#xff0c;一个印度裔的硅谷中…

【LeetCode】修炼之路-0006-Zigzag Conversion (Z 字形变换)【python】

题目 The string “PAYPALISHIRING” is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility) P A H N A P L S I I G Y I R And then read line by line: “PAHNAPLSIIGYIR” …

荣耀电脑管家-系统重装之查询设备序列号

winr输入cmd&#xff0c;再命令行中输入 wmic bios get serialnumber 如下所示