完整教程:多线程——单例模式

news/2025/9/27 16:54:49/文章来源:https://www.cnblogs.com/ljbguanli/p/19115301

完整教程:多线程——单例模式

目录

1.设计模式 - 单例模式

2.饿汉模式

3.懒汉模式

3.1 初始版-非线程安全

3.2 synchronized 修饰

3.3 双重 if

3.4 volatile 修饰

4.小结


经过几期的多线程学习,已经对多线程有了一定的了解。从这期开始,将会对多线程的使用做一个更深入的探讨。


1.设计模式 - 单例模式

设计模式是一个抽象的概念,是指软件设计中针对常见问题的可重用解决方案。

大家都一定听说过高斯计算从 1 加到 100 的故事,有一天,高斯的数学老师布置了一道从1加到100的习题,想让学生们算一整节课,结果刚说完题目高斯就报出了答案。 原来,他发现数列两头可以一 一配对:1+1002+99……每一对的和都是101,共有50对,所以总和是5050。后面这个问题就逐渐的演变为我们今天熟悉的等差数列。我们对等差数列并不陌生,但是如果前人发现这个规律,可能我们一时间也不会想起来这种规律,属于是站在巨人的肩膀上学习了。

设计模式就是这样的场景,在计算机中,一些大佬们会针对一些常用典型的场景,为了避免重复造轮子,就设计出了对应的典型的解决方案,这就是设计模式,相当于公式化,后人在使用时直接”套公式“,就不会让这种问题的解决方案差到哪里去。单例模式就属于典型的设计模式之一。

本期主要讲的就是单例模式。单例模式是运行程序中的某个类只有一个实例,也就是只会 new 一次对象。比如一个学校只有一位正校长,一位书记,但是副校长可以有很多人,人体也只有一颗心脏。单例模式可以确保实例唯一、只会创建一次对象节省资源等。

单例模式最常见的有“饿汉模式”和“懒汉模式”,下面将逐一介绍这两种模式。

2.饿汉模式

先从字面来理解,我们什么时候会形容一个人是“饿汉”?肯定是看到一个人在吃饭时狼吞虎咽,恨不得几口饭当一口饭吃的感觉,如果是这种情况,说明这个人可能非常“饥饿”。

在计算机单例模式中,如果一个类在加载时就创建了实例,就叫“饿汉模式”。这种模式因其“急切”初始化的特性而命名为“饿汉”,因为只要程序一运行就创建了,都不知道会不会使用它。

怎么实现“饿汉模式”?首先要抓住“急切”的特性,既然是类在加载时就创了实例,那就说明不论是否需要这个类的实例,只要该类加载就会创建都要创建,并且在外部不能再对其实例化。先来看代码:

class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton() {
}
}
public class Single1 {
public static void main(String[] args) {
}
}

代码解读:主要看 class Singleton 这个类

  • 首先定义这个类的静态成员 instance静态变量初始化在类初始化阶段完成,早于任何线程访问,类加载往往是程序一启动就触发,也就是说程序一启动,这个实例就被创建了
  • 提供 getInstance 方法当线程想要使用这个实例时,可以通过这个方法获取获取实例,如果多线程一起调用,它们得到的对象是一样的,因为定义 instance 时就已经实例化,,不会出现竞争的情况,这里也说明了“饿汉模式”是天然线程安全的
  • 构造方法,这是单例模式的点睛之笔,在以往学习任何一个类的构造方法时,查看源码都可以看见这些构造方法是被 public 修饰,而我们设计单例模式时,构造方法必须用 private 修饰,这就使得这个类将不再被实例化

测试:

测试1中,定义两个 Singleton 变量,用 == 判断它们是否相等,根据结果可以看到返回的是 true,说明它们得到的对象是相等的,当我们查看哈希值时,哈希值是相等的,这就说明了这两个变量实际指向的是同一个对象。

测试2中,我们尝试实例化 Singleton 对象,但是编译失败,这就是 private 修饰构造方法的点睛之笔。

当然,这个代码实现实例化的对象是无参的,如果想要传参数,那么在一开始定义静态成员 instance 实例化的时候就可以传参数进去,这里不再演示。

果还记得反射这一知识点,可能会有的小伙伴认为这个模式可以被反射攻击。确实会的,比如以以下是通过反射的方式尝试进行修改,导致结果是 false,并且哈希值也不一样:

怎么防御反射攻击?在构造方法里抛一个异常:

class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
// 防止反射攻击
if (instance != null) {
throw new RuntimeException("单例模式禁止通过反射创建实例");
}
}
public static Singleton getInstance() {
return instance;
}
}

因为“饿汉模式”是类加载时就已经创建实例,而反射攻击时在这个阶段之后:

Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton illegalInstance = constructor.newInstance(); // 这里会抛出异常

这里就相当于有一个时间差,即类加载初始化在反射调用之前,可以确保反射调用时 instance 就已经存在,并且 instance 被 final 修饰,可以确保实例引用时不会被反射修改。

这只是简单的防御措施,针对高强度攻击依然无法防御,这里不过多深究,本期主要是了解单例模式的设计。

3.懒汉模式

3.1 初始版-非线程安全

有了饿汉模式的基础,其实懒汉就不难理解了。“懒汉”则说明它非常懒,喜欢“摆烂”,只要被催促的时候才会行动起来。

在计算机单例模式中,一个类只有在第一次请求时才会被创建,叫做懒汉模式。

我们先仿照“饿汉模式”的代码,把“懒汉模式”的代码整体框架写出来(这个一个初始版本):

class SingletonLazy {
private static SingletonLazy instance;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
public class Single2 {
public static void main(String[] args) {
}
}

代码解读

  • 首先定义这个类的静态成员 instance,因为是只有被需要的时候才会被创建实例,所以这里不用 final 修饰,同时也不进行初始化,默认为 null
  • 提供 getInstance 方法当线程想要使用这个实例时,可以提供这个获取获取实例,由于定义的时候并没有进行初始化,默认为 null,所以在这里调用这个方法的时候,要先进行判断是否而空(可能多次调用,单例模式只有一个实例,不能多次创建),是空就实例化,后续再调用时就不会再进行实例化。
  • 构造方法,这是单例模式的点睛之笔,在以往学习任何一个类的构造方法时,查看源码都可以看见这些构造方法是被 public 修饰,而我们设计单例模式时,构造方法必须用 private 修饰,这就使得这个类将不再被实例化

3.2 synchronized 修饰

与“饿汉模式”不同的是,“懒汉模式”的实例化是在 getInstance 方法里,这里有 new 和 == ,涉及到修改操作,是非原子的,而单例模式下实例只能有一份,所以这种方式存在线程安全问题。如何解决?

对于非原子的线程安全问题,可以进行加锁 synchronizedsynchronized 一是可以放在方法中直接修饰 getInstance二是可以放在 if 判断条件的外面(如果把 synchronized 放在 if 判断里,是起不到作用的,因为涉及到的非原子问题主要是 == 导致)。这里以第二种方式实现(为什么不用第一种,后文会介绍)

class SingletonLazy {
private static SingletonLazy instance;
private static  Object locker = new Object();//锁
private SingletonLazy() {}
public static SingletonLazy getInstance() {
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
}
public class Single2 {
public static void main(String[] args) {
}
}

3.3 双重 if

我们知道加锁是为了让一个线程阻塞等待持有锁的线程先执行。但是,上面展示的代码,如果每次希望调用一次  getInstance 方法,是不是意味着每次都要有加锁和释放锁的时间等待,对于计算来讲,这个时间是意味着很久的,在多线程下,这种加锁会造成相互堵塞,影响了程序的运行效率。

怎么解决这个问题?按需加锁。按需加锁就是有需要的时候再加锁,涉及线程安全问题时再加锁,不涉及就不再加锁,而什么情况下涉及线程安全问题?就是在 instance 为 null 的时候,这个判断 == 以及在 new 对象的时候,会涉及到线程安全问题。当 instance 不为 null 的时候,是不是就意味着不需要加锁了?所以,这个时候我们 synchronized 的外层再用一个 if 条件判断 instance 是不是为 null,如果是,就加锁,如果不是,就不加锁,这就是双重 if

class SingletonLazy {
private static SingletonLazy instance;
private static  Object locker = new Object();//锁
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (instance == null) {
//第一次if
synchronized (locker) {
if (instance == null) {
//第二次if
instance = new SingletonLazy();
}
}
}
return instance;
}
}

这两个 if 的作用不同:第一个 if 是判断 instance 是否为空,如果为空才加锁,并创建实例,如果不为空,那么整个第一个 if 的代码块都不再执行,减少了加锁和释放锁的时间,第二个 if 也是判断 instance 是否为空,但这里的目的是创建实例,在锁内。

3.4 volatile 修饰

现在,上面的版本已经解决了一部分的线程安全问题。既然说一部分,那么就说明线程安全还没有达到预期(这里只考虑单例模式下“懒汉模式”的设计,不再考虑反射或者其它情况的攻击)。

我们说造成线程安全问题的原因主要有五条:

  1. 操作系统对线程的调度是随机的、抢占式执行的(根本原因)
  2. 多个线程同时修改同一个变量
  3. 修改变量的操作不是原子的
  4. 内存可见性问题
  5. 指令重排序问题

第1条是根本原因,我们无法改变,第2条在多线程情况下我们有时候就希望改变同一个变量,也无法改变,现在第3条已经被我们解决了,第4条和第5条用我们的角度看待解决方式是一样的。接下来分析一下会不会出现第4条和第5条的线程安全问题情况。

如果有两个线程,线程1在读取 instance 时,线程2会不会已经对它进行修改而线程1不知道呢?这肯定会存在的,因为线程1在读取时,可能判断 instance == null 条件是成立的,这会导致线程2的修改无法及时将修改的共享变量返回主内存,导致线程1再创建一个实例,这是会发生错误的,所以这里的“内存可见性问题”取决于编译器的优化,为了稳妥起见,我们可以用 volatile 修饰。

但不是说用 volatile 修饰 instance 后,就不考虑指令重排序的问题,因为这两个问题的解决方案是一致的。接下来分析一下指令重排序的情况:

我们直到指令重排序也是编译器优化的一种提现方式,会在保证逻辑不变的前提下,调整代码的执行顺序以达到提升性能的效果。但是在实例化对象的改成中,会涉及到三个步骤:1)申请内存空间;2)在这个内存空间上构造对象(初始化);3)将引用赋值给 instance (这个时候 instance 不再是 null)。但在编译器的优化下,如果发生指令重排序,就有可能把顺序调整成①③②,这在单线程条件下不需要担心,但在多线程情况下,如果线程发生顺序调整,就会出现bug,错误时间点如下:

所以,在这种情况下,就需要用 volatile 修饰 instance,而不是简单的“内存可见”问题,这里的 volatile 主要解决的问题就是“指令重排序”问题。

class SingletonLazy {
private volatile static SingletonLazy instance;
private static  Object locker = new Object();//锁
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (instance == null) {
//第一次if
synchronized (locker) {
if (instance == null) {
//第二次if
instance = new SingletonLazy();
}
}
}
return instance;
}
}

这样的版本才是真正完成了“懒汉模式”的线程安全问题。关于测试,可以和“饿汉模式”类似,这里也不再进行演示。

4.小结

设计模式是指软件设计中针对常见问题的可重用解决方案。单例模式就属于典型的设计模式之一。

单例模式最常见的有“饿汉模式”和“懒汉模式”。

如果一个类在加载时就创建了实例,就叫“饿汉模式”;一个类只有在第一次请求时才会被创建,叫做“懒汉模式”。

“懒汉模式”需要注意线程安全问题和双重 if 的含义。

“饿汉模式”和“懒汉模式”各有优点,比如“饿汉模式”最简单,在类加载时就创建,可以认为天然线程安全,但这种可能造成一定的资源浪费,而“懒汉模式:比较复杂,需要考虑多种情况下才能避免线程安全问题,虽然有双重 if ,但只有在被使用时才会被创建实例,可以认为比”饿汉模式“节省一定的资源。


单例模式实现的方式有很多,最常见的就是本期介绍的“饿汉模式”和懒汉“模式”。下期将介绍设计模式的第二种模式:阻塞队列。我们知道队列是一种先进先出的数据结构,那阻塞队列是什么?队列为什么会发生阻塞?欲知后事如何,且听下回分解!

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

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

相关文章

A Twisty Movement

CF933A A Twisty Movement 简化题目 给定一个有 \(1\),\(2\) 两个数字组成的数组中,选择一个子串,将 \(1\) 变成 \(2\),将 \(2\) 变成 \(1\),求出变化后的序列的最长上升子序列。 思路 简单的情况 如果没有变换操…

佛山新网站建设渠道天津工程网站建设

MySQL 中的集群部署方案 前言 这里来聊聊,MySQL 中常用的部署方案。 MySQL Replication MySQL Replication 是官方提供的主从同步方案,用于将一个 MySQL 的实例同步到另一个实例中。Replication 为保证数据安全做了重要的保证,是目前运用…

山东网站建设设计小清新个人网站

七、高并发内存池–Page Cache 7.1 PageCache的工作原理 PageCache是以span的大小(以页为单位)和下标一一对应为映射关系的哈希桶,下标是几就说明这个哈希桶下挂的span的大小就是几页的,是绝对映射的关系。因为PageCache也是全局只有唯一一个的&#x…

完整教程:iOS 混淆与反调试反 Hook 实战,运行时防护、注入检测与安全加固流程

完整教程:iOS 混淆与反调试反 Hook 实战,运行时防护、注入检测与安全加固流程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-f…

3.WPF - 依赖属性 - 实践

3.WPF - 依赖属性 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", &q…

Attention进阶史(MHA, MQA, GQA, MLA)

在深度学习领域,注意力机制(Attention Mechanism)自诞生以来便成为推动自然语言处理和计算机视觉等任务发展的核心动力。从最初的多头注意力(MHA)到如今的高效变体,如多查询注意力(MQA)、分组查询注意力(GQA)…

实用指南:AI编程时代的变革:Replit CEO对话深度解析

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025北京个性旅行自由行口碑推荐北京汇通清源国际旅游公司,满足独特需求,自由随心

2025年,想要在北京开启一场个性十足的自由行?那就一定要了解一下北京汇通清源国际旅游公司。这家成立于2014年的旅游公司,注册资本300万元人民币,坐落于北京市朝阳区,业务广泛,涵盖全北京各个区域的旅游业务,包…

广州专业做网站多少钱加速游戏流畅的软件

今天我来讲一下在Linux下各环境的搭建,主要就讲一下jdk、MySQL、和一个代理服务器nginx 1、 jdk的安装配置 1)卸载自带openjdk 当我们拿到一个全新的ECS的时候上面有的会自带一个openjdk,但是我们一般不会用这个,所以在这里我们会先卸载这个自…

电子商务网站开发文档在线qq登录无需下载

选择、插入、冒泡三种算是最典型的排序算法了,空间复杂度都为O(1) 选择排序时间复杂度跟初始数据顺序无关,O(n2),而且还不稳定; 插入排序时间复杂度跟初始数据顺序有关最好O(n),最坏O(n2),稳定 冒泡排序时间复杂度跟初始数据顺序有…

网站 自定义表单招聘网站开发背景

陆游的《诗人苦学说》:从藻绘到“功夫在诗外” 今天看万维钢的《万万没想到》一书,看到陆游的功夫在诗外的句子,特意去查找这首诗的原文。故而有此文。 我国学人还往往过分强调“功夫在诗外”这句陆游的名言,认为提升综合素质是一…

学做网站多少钱百度关键词搜索广告的优缺点

目录 引言整体结构图方法介绍训练vision vocabulary阶段PDF数据目标检测数据 训练Vary-toy阶段Vary-toy结构数据集情况 引言 论文:Small Language Model Meets with Reinforced Vision Vocabulary Paper | Github | Demo 说来也巧,之前在写论文阅读&…

2025推拉门品牌推荐榜:聚焦玻璃推拉门,钛镁合金推拉门选择指南

随着家居品质需求升级,推拉门已成为阳台封窗、厨房隔断等场景的核心配置,但市场现状却让消费者陷入选择困境:部分产品宣称的隔音节能性能与实际体验严重不符,五金件易损耗、密封失效等质量问题频发,售后 “踢皮球…

C++中函数的分文件编写

C++中函数的分文件编写Posted on 2025-09-27 16:40 steve.z 阅读(0) 评论(0) 收藏 举报1、创建 .h 头文件 2、创建 .cpp 源文件 3、在 .h 头文件中,编写函数声明 4、在 .cpp 源文件中,编写函数定义 test.h #inc…

PyTorch详细安装指南与常见问题解决强大的方案

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

做网站大概一个月多少工资政务信息网站建设方案

作者 | butterfly100来源 | cnblogs.com/butterfly100/p/9034281.html一. 数据切分关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索…

礼县建设局网站重庆公路工程建设信息管理系统

加入大语言模型(LLM) 接着,需要在聊天机器人中加入 LLM。这样,用户就可以和聊天机器人开展对话了。本示例中,我们将使用 OpenAI ChatGPT 背后的模型服务:GPT-3.5。 聊天记录 为了使 LLM 回答更准确,我们需要存储用户和机器人的聊天记录,并在查询时调用这些记录,可以用…

【Azure APIM】APIM在上传文件的时候,请求的Payload是否有文件大小的限制呢?

APIM在上传文件的时候,请求的Payload是否有文件大小的限制呢?问题描述 使用APIM + App Service的架构对外提供服务,其中一个接口为文件上传。在测试的时候,发现上传超过20MB的内容时候就会遇见报错,而不使用APIM时…

PolarDN PIoTS 简单

PolarD&N PIoTS 简单bluetooth test 一、查看题目附件有一个log文件和一个txt文件。 二、题目分析 根据txt文件提示,使用FrontLine11工具打开log文件使用FrontLine11进行分析,只有command指令才能控制设备,筛选…