【多线程】单例模式

embedded/2025/3/18 12:45:00/

文章目录

1. 单例模式

1.1 什么是单例模式

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

单例 =单个实例(对象)

1.2 为什么使用单例模式

使用单例模式,就可以对咱们的代码进行一个更严格的校验和检查。
示例:

有的时候代码中,需要使用一个对象,来管理/持有大量的数据,此时有一个对象就可以了。比如,一个对象管理了10G的数据,如果你不小心创建出多个对象,内存空间就会成倍增长,机器就顶不住了。

期望:

让机器 (编译器)能够对代码中的指定的类,创建的实例个数进行校验,如果发现创建多个实例了,就直接编译报错这种。如果能做到这一点,就可以非常放心的编写代码,不必担心因为失误创建出多个实例了。

1.3 实现单例模式

1.3.1 饿汉模式

饿汉模式是单例模式的一种实现方式,其核心特点是在类加载时就创建单例对象,确保在程序运行期间该类的单例对象始终存在。

饿汉模式在多线程中天然就是线程安全

class Singleton {private static Singleton instance = new Singleton();public static Singleton getInstance() {return instance;}private Singleton() {}
}
public class Danli1 {public static void main(String[] args) {// Singleton s = new Singleton();Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1 == s2);}
}

这个引用就是我们希望创建出唯一实例的引用。static 静态的,指的是类属性, instance就是Singleton类对象里面持有的属性。每个类的类对象只有一个,类对象的static属性自然也就只有这一个。

private static Singleton instance = new Singleton();

这段代码的作用就是想要使用这个类的实例,就需要通过这个方法来获取实例。不应该在其他的代码中重新new这个对象,而是直接调用这个方法来获取到现成的对象

public static Singleton getInstance() {return instance;}

单例模式的点睛之笔,为了防止其他类new一个对象,这里直接将构造方法私有化,其他类调用不了构造方法自然也就创建不了这个实例。

1.3.1 懒汉模式

懒汉模式是单例模式的一种实现方式,其核心特点是在第一次需要使用单例对象时才进行实例化,而不是像饿汉模式那样在类加载时就创建实例,这样可以节省资源。

package Thread;/*** 懒汉模式* */
class SingletonLazy {private static Object locker = new Object();private volatile static SingletonLazy instance = null;//注意这个volatile关键字的使用public static SingletonLazy getInstance() {//如果instance为null,就说明是首次调用,首次调用需要考虑到线程安全的问题,就要加锁//如果是非null,就说明是后续的操作,就不必加锁if (instance == null) {synchronized (locker){if (instance == null) {instance = new SingletonLazy();}}}return instance;}private SingletonLazy() {}
}
public class Danli2 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);}
}

关键问题:在多线程中,并发调用getInstance方法,这两个代码是否线程安全?

对于饿汉来说,getlnstance 直接返回 Instance 实例。这个操作本质上就是"读操作",线程安全。
对于懒汉来说,getlnstance 先判断再创建 Instance 实例。这个操作有读也有写,线程不安全(上述代码通过修改改为线程安全)。

修改方案:
1)加锁
在多线程中,有可能线程1执行到判断,切换到了线程2执行完了所有创建实例,这时切换回线程1时,线程1在已经判断的基础上又创建了新的实例,bug出现!!!

为了让代码执行正确,将 if 和 new 两个操作,打包成一个原子

synchronized (locker){if (instance == null) {instance = new SingletonLazy();}}

2)双重校验锁
在多线程中,如果instance已经被创建,应该快速的返回该实例。但是判断操作还是在锁里,每次调用都先加锁再解锁,此时效率就非常低了!!!(因为加锁就意味着可能会产生阻塞)

为了提高效率,在锁的外层加一个判断,判定是否需要加锁。如果是第一次调用该方法,也就是创建实例对象,则进行加锁解锁;如果是第二次之后调用该方法,则仅需返回第一次创建好的实例对象就行,提高效率。

    public static SingletonLazy getInstance() {//如果instance为null,就说明是首次调用,首次调用需要考虑到线程安全的问题,就要加锁//如果是非null,就说明是后续的操作,就不必加锁if (instance == null) {synchronized (locker){if (instance == null) {instance = new SingletonLazy();}}}return instance;}

3)指令重排序
指令重排序就JVM编译器优化的一种方式,调整原有代码的执行顺序,保证逻辑不变的情况下,提高程序的效率,但是在提升效率的同时会引起线程安全问题。

instance = new SingletonLazy();

对于上面这一个行代码,可以拆分为三大步骤:

(1)申请一段内存空间
(2)在这个内存上调用构造方法,创建出这个实例
(3)把这个内存地址赋值给 instance 引用变量

正常情况下,上述代码是按照1.2.3的顺序执行的,但是编译器也会优化成1.3.2的顺序执行,此时,指令重排序就会造成问题。
如果线程1先执行1.3,调度走,此时instance不是null,但是指向的其实是一个尚未初始化的对象。但是未初始化的对象会被线程2判定为 instance != null ,就会直接 return。如果线程2继续使用 instance 里面的属性和方法,就会出现问题(此时这里的属性都是未初始化的“全0”值)。就可能会引起代码的逻辑出现问题。

为了解决这问题,还是引入 volatile
volatile 有两个功能
1.保证内存可见性,每次访问变量必须都要重新读取内存,而不会优化到寄存器/缓存中
2.禁止指令重排序。针对这个被 volatile 修饰的变量的读写操作相关指令,是不能被重排序的!!!

private volatile static SingletonLazy instance = null;//注意这个volatile关键字的使用

http://www.ppmy.cn/embedded/173594.html

相关文章

vue2:el-table列中文字前面加icon图标的两种方式

1、文字前面加icon <el-table-column label="姓名" align="left" prop="nickName"><template #default="{ row }"><i v-if="row.sync" class="el-icon-lock"></i><span>{{ row.nic…

网络安全与七层架构

网络安全与七层架构 随着互联网技术的迅猛发展&#xff0c;网络安全问题日益凸显。网络安全不仅影响到个人用户的信息安全&#xff0c;更是企业及国家安全的重要组成部分。而七层架构&#xff08;OSI模型&#xff09;为网络通信提供了理论支撑&#xff0c;能够有效地帮助我们理…

ImGui 学习笔记(五) —— 字体文件加载问题

ImGui 加载字体文件的函数似乎存在编码问题&#xff0c;这一点可能跟源文件的编码也有关系&#xff0c;我目前源文件编码是 UTF-16。 当参数中包含中文字符时&#xff0c;ImGui 内部将字符转换为宽字符字符集时候&#xff0c;采用的 MultiByteToWideChar API 参数不太对&#…

处理变长的时间序列

pytorch中torch.nn.utils.rnn相关sequence的pad和pack操作 官网…torch.nn.utils.rnn.pack_padded_sequence 知乎pack_padded_sequence 和 pad_packed_sequence 结论 ✅ pack_padded_sequence 是最好的方法&#xff08;避免无效计算&#xff0c;提升性能&#xff09; &#x…

java 动态赋值写入word模板

最近工作中&#xff0c;客户给提供了word模板&#xff0c;要求动态赋值到word模板中&#xff0c;查阅相关资料&#xff0c;最终完成了需求&#xff0c;希望也可以帮助到大家&#xff01; 例如表格如下&#xff1a; 第一步&#xff1a;在word模板中&#xff0c;把需要动态赋值的…

ThreadLocal

多线程和 ThreadLocal 是 Java 并发编程中的两个重要概念&#xff0c;它们在处理线程安全和资源隔离时扮演关键角色。 1. 多线程基础 1.1 什么是多线程&#xff1f; 线程&#xff1a;是操作系统能够调度的最小执行单元&#xff0c;一个进程可以包含多个线程。 多线程&#xf…

高亮动态物体——前景提取与动态物体检测器(opencv实现)

目录 代码说明 1. 导入库 2. 创建背景建模对象 3. 打开视频源 4. 逐帧处理视频 5. 应用背景建模获得前景掩码 6. 形态学操作去除噪声 6.1 定义形态学核 6.2 开运算去除噪点 6.3 膨胀操作填补前景区域空洞 7. 轮廓检测识别动态物体 8. 绘制轮廓和边界框 9. 显示处理…

Spring Boot 的自动装配

Spring Boot 的自动装配&#xff08;Auto Configuration&#xff09;是其核心特性之一&#xff0c;通过智能化的条件判断和配置加载机制&#xff0c;极大简化了传统 Spring 应用的配置复杂度。其原理和实现过程可概括为以下几个关键点&#xff1a; 一、核心触发机制&#xff1a…