【多线程】线程安全的单例模式

news2025/1/20 14:59:36

线程安全的单例模式

  • 饿汉模式
  • 懒汉模式
    • 单线程版
    • 多线程版
    • 多线程版(改进)

单例模式能保证某个类在程序中只存在 唯一 一份实例, 而不会创建出多个实例,从而节约了资源并实现数据共享。
比如 JDBC 中的 DataSource 实例就只需要一个.

单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.

饿汉模式

类加载的同时, 创建实例.

    class Singleton {
        private static Singleton instance = new Singleton();
        // 私有化构造方法,防止外部创建实例
        private Singleton() {}
        public static Singleton getInstance() {
            return instance;
        }
    }

注意:

  1. 使用 static 修饰 instance,该实例就是该类的唯一实例。
  2. 要私有化构造方法,防止外部创建实例。
  3. 饿汉模式中,线程只读取了实例,所以是线程安全的。

懒汉模式

单线程版

类加载的时候不创建实例. 只有真正第一次使用它的时候才创建实例.

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

多线程版

上面的懒汉模式的实现是线程不安全的. (线程安全问题详解)

因为这里面 读取 和 修改 instance 是两个操作,不是原子操作,线程安全问题发生在首次创建实例时.
如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.

在这里插入图片描述

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

注意:
针对 Singleton 类对象加锁(类对象在一个程序中只能有一份),保证所有线程调用 getInstance 方法时,针对同一个对象进行加锁。

多线程版(改进)

代码可能出现线程安全问题的时机就在第一次创建实例时,一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改 instance 了) ,按照上面的加锁方式,不管是否会发生线程安全问题都会加锁,即使初始化之后线程安全了,仍然存在大量锁竞争,降低了程序的效率。

所以在加锁的基础上, 做出了进一步改动:

  • 使用双重 if 判定, 降低锁竞争的频率 。
  • 给 instance 加上了 volatile, 保证内存可见性以及防止指令重排序。
class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么要双重 if 判定 ?

加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全 只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了.

外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了,如果已经创建出来了就不用再加锁了。

为什么要使用 volatile ? volatile 关键字详解

  1. 保证内存可见性:(内存可见性问题)

多个线程调用 getInstance 方法,就会造成大量读 instance 内存操作,这样就可能导致编译器进行优化,不读内存,直接读寄存器,一旦优化,即使其他线程创建了实例,该线程也感知不到。所以使用 volatile 关键字。
(主要针对外外层的 if 判断,因为 synchronized 也能防止指令重排序,所以 内层判断不会受影响。)

  1. 防止指令重排序

什么是指令重排序?
举个栗子:
一段代码是这样的:

1. 去前台取下 U2. 去教室写 10 分钟作业
3. 去前台取下快递

为了提高效率, JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台,提高效率。这种就叫做指令重排序。

编译器对于指令重排序的前提是 “保持逻辑不发生变化”.
这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了,
多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测,
因此激进的重排序很容易导致优化后的逻辑和之前不等价.

在这里插入图片描述

其中创建实例 new Singleton() 又分为 三个步骤:

  1. 分配内存空间
  2. 对内存空间进行初始化
  3. 把内存空间的地址赋给引用 instance

假如没有使用 volatile 关键字,编译器可能对此进行了优化,进行了指令重排序,那么有可能优化为 1 -> 3 -> 2 。

这样的话,当第一个线程 t1 要获取实例时,因为实例为null, 所以肯定会创建实例,但是可能编译器进行了优化,那么可能顺序就变成了 1 -> 3 -> 2

  • 先开辟了一块空间
  • 将空间地址赋值给引用
  • 对空间初始化

当进行完第二步,把空间地址赋值给引用后,还没来得及初始化,此时另外一个线程 t2 来获取实例了, 进行判断时,发现 instance 不为空,那么就直接返回实例了
在这里插入图片描述

t2 拿到实例后,直接进行使用,那么就会报错了,因为虽然开辟了空间,但是 t1 还没来得及对空间进行初始化,拿到的是不完整的对象。

解决:
对 instance 对象加上 volatile 关键字,禁止指令重排序,保证其他线程拿到的是一个完整的实例。

完整过程举栗:

  1. 有三个线程, 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没有创建的消息. 于是开始竞争同一把锁.

  2. 其中线程1 率先获取到锁, 此时线程1 通过里层的 if (instance == null) 进一步确认实例还没有创建, 于是就把这个实例创建出来.

  3. 当线程1 释放锁之后, 线程2 和 线程3 也拿到锁, 也通过里层的 if (instance == null) 来确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了.

  4. 后续的线程, 不必加锁, 直接就通过外层 if (instance == null) 就知道实例已经创建了, 从而不再尝试获取锁了. 降低了开销.

总结:

  1. 构造方法私有化,防止外部创建实例。
  2. 使用 static 修饰,保证是该类的唯一实例。
  3. 使用 volatile 修饰,保证内存可见性以及防止指令重排序。
  4. 双重 if 判断,第一次判断是否需要加锁,从而降低锁竞争,提高效率。
    第二层 if 判断是否真的需要创建实例。
  5. 使用 synchronized 进行加锁,防止第一次创建实例时由于线程安全问题而创建出多个实例。

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

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

相关文章

Unity3D URP 仿蜘蛛侠风格化BloomAO

Unity3D URP 仿蜘蛛侠风格化Bloom&AO BloomBloom效果流程:制作控制面板VolumeComponent.CSCustom Renderer FeatherCustom Renderer PassBloom ShaderComposite Shader 完善Custom Feather风格化AO 总结 本篇文章介绍在URP中如何进行风格化后处理,使…

【MATLAB第74期】#源码分享 | 基于MATLAB的ARX-ARMAX线性自回归移动平均外生模型(结合最小二乘思路)

【MATLAB第74期】#源码分享 | 基于MATLAB的ARX-ARMAX线性自回归移动平均外生模型(结合最小二乘思路) 根据ARX预测输出和实际输出的误差向量,采用ARMAX算法结合ARX误差建模,对预测值进一步细化。通过将误差描述为白噪声的移动平均…

Spring事务管理: 构建稳健的数据库事务处理

🌷🍁 博主猫头虎(🐅🐾)带您 Go to New World✨🍁 🦄 博客首页——🐅🐾猫头虎的博客🎐 🐳 《面试题大全专栏》 🦕 文章图文…

C++算法 —— 动态规划(4)子数组

文章目录 1、动规思路简介2、最大子数组和3、环形子数组的最大和4、乘积最大子数组5、乘积为正数的最长子数组长度6、等差数列划分7、最长湍流子数组8、单词拆分9、环绕字符串中唯一的子字符串 每一种算法都最好看完第一篇再去找要看的博客,因为这样会帮你梳理好思路…

商城系统优化

1、DB、模板的渲染速度(thymeleaf)、静态资源、日志、JVM 数据库的优化(参照数据库优化课程)使用索引,减少数据库的交互次数、缓存 thymeleaf使用缓存 静态资源:放到nginx中,实现动静分离 2、…

【数学】ABC 319 E

E - Bus Stops 题意: 思路: 感觉思路比较简单 首先注意到每个询问的范围是1e9,不难想到答案一定存在某个循环节,最后一定是要 %T的 那么问题就在于找到这个循环节是什么 猜想循环节为lcm(p1, p2, p3, ....) 用小数据验证 n…

一篇博客教会您SpringMVC文件上传、下载,多文件上传及工具jrebel的使用

目录 一.文件上传 二.文件下载 三.多文件上传 四,jrebel的介绍 前言: 我们之前已经实现了SpringMVC的增删改查,今天这一篇博客教会您SpringMVC文件上传、下载,多文件上传及工具jrebel的使用,希望这篇博客能够给正在…

二、Spark 调度系统

目录 Spark 调度系统DAGSchedulerSchedulerBackendTaskSchedulerExecutorBackendSpark 任务调度流程 Spark 调度系统 分布式计算的精髓,在于如何把抽象的计算图,转化为实实在在的分布式计算任务,然后以并行计算的方式交付执行。 Spark调度系…

Mojo安装使用初体验

一个声称比python块68000倍的语言 蹭个热度,安装试试 系统配置要求: 不支持Windows系统 配置要求: 系统:Ubuntu 20.04/22.04 LTSCPU:x86-64 CPU (with SSE4.2 or newer)内存:8 GiB memoryPython 3.8 - 3.10g or cla…

华为云云耀云服务器L实例评测 | 分分钟完成打地鼠小游戏部署

前言 在上篇文章【华为云云耀云服务器L实例评测 | 快速部署MySQL使用指南】中,我们已经用【华为云云耀云服务器L实例】在命令行窗口内完成了MySQL的部署并简单使用。但是后台有小伙伴跟我留言说,能不能用【华为云云耀云服务器L实例】来实现个简单的小游…

车载诊断数据库——诊断问卷调查表与CDD关联关系

车载诊断数据库——诊断问卷调查表与CDD关联关系 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 没有人关注你。也无需有人关注你。你必须承认自己的价值,你不能站在他人的角度来反对自己。人生…

超级电容-电池-超级电容混合储能系统能量管理simulink仿真建模模型

建立混合储能系统模型 在Simulink中,首先需要建立一个超级电容和蓄电池并联的混合储能系统模型。其中,超级电容和蓄电池的荷电状态(SOC)需要根据实际情况进行管理。荷电状态可以通过对电池和超级电容的电压、电流等进行测量&…

说透 Nacos 一致性协议

1 Nacos ⼀致性协议 1.1 为什么 Nacos 需要⼀致性协议 Nacos尽可能减少用户部署以及运维成本,做到用户只需要⼀个程序包,就快速单机模式启动 Nacos 或集群模式启动 Nacos。而 Nacos 是⼀个需要存储数据的组件,为实现目标,就要在…

透视俄乌网络战之二:Conti勒索软件集团(上)

透视俄乌网络战之一:数据擦除软件 Conti勒索软件集团(上) 1. Conti简介2. 组织架构3. 核心成员4. 招募途径5. 工作薪酬6. 未来计划参考 1. Conti简介 Conti于2019年首次被发现,现已成为网络世界中最危险的勒索软件之一&#xff0…

汇川PLC学习Day3:轴控代码编写、用户程序结构说明与任务配置示例、

汇川PLC学习Day3:轴控代码编写、用户程序结构说明、任务配置示例 一、新建轴与轴控代码编写 1. 新建轴 (1)新建一个轴 (2)将轴名字更新为实际名字 可以后面实例化后再更改,汇川可以在更新名字时同步更新其他编写的代码名字&a…

GStreamer时钟同步

播放复杂媒体时,每个audio和video sample必须在特定时间按特定顺序播放。为此,GStreamer提供了一种同步机制,通过使用 GstClock object、buffer timestamps和SEGMENT event来实现: (1)GstClock:…

Java中如何获取一个字符串是什么类型

Java中如何获取一个字符串是什么类型? 在Java中,您可以使用一些方法来确定一个字符串的类型。下面是一些常用的方法: 使用正则表达式:您可以使用正则表达式来匹配字符串是否符合特定的模式或格式,以确定其类型。例如&…

【Linux入门指北】Linux磁盘扩容

文章目录 1、给 / 分区扩容 Linux在使用过程中由于数据量不断增大,导致磁盘空间不足,需要增加磁盘空间,主要有以下三种方式: 直接给 / 分区(或者某一分区)扩容,直接在原有磁盘上增大空间给虚拟机新增一块磁…

typeScript 学习笔记(二)

类接口 TypeScript 入门教程 (xcatliu.com) 十四.类 ① 类 类:定义了一件事物的抽象特点,包含它的属性和方法对象:类的实例,通过new生成面向对象(OOP)的三大特性:封装、继承、多态封装&…