多线程代码案例-1 单例模式

单例模式

单例模式是开发中常见的设计模式。

设计模式,是我们在编写代码时候的一种软性的规定,也就是说,我们遵守了设计模式,代码的下限就有了一定的保证。设计模式有很多种,在不同的语言中,也有不同的设计模式,设计模式也可以被认为是对编程语言语法的补充

单例即单个实例(对象),某个类在一个进程中,只应该创建出一个实例(原则上不应该创建出多个实例),使用单例模式,可以对我们的代码进行一个更为严格的校验和检查。

举个例子:有时候,代码中需要管理/持有大量的数据,此时有一个对象就可以了。比如:我需要一个对象管理10G的数据,如果我们不小心创建出多个对象,内存空间就会成倍地增长。

如何保证只有唯一的对象呢?我们可以选择“君子之约地方式”,写一个文档,文档上约定,每个接手维护代码的程序员,都不能对这个类创建多个实例(很显然,这种约定并不靠谱)我们期望让机器(编译器)能够对代码中的指定类,对创建的实例个数进行检验。如果发现创建出了多个实例,就直接编译报错,但是Java语法中本身没有办法直接约定某个对象能创建出几个实例,那么就需要程序员使用一些技巧来实现这样的效果。

实现单例模式的方式有很多种,这里介绍两种实现方式:饿汉模式和懒汉模式。

1 饿汉模式

代码如下:

//饿汉模式
//期望这个类只能有唯一的实例(一个进程中)
class Singleton{private static Singleton instance = new Singleton();//在这个类被加载时,就会初始化这个静态成员,实例创建的时机非常早——饿汉public static Singleton getInstance(){//其他代码想要使用这个类的实例就需要通过这个方法进行获取,// 不应该在其他代码中重新new这个对象而是使用这个方法获取这个现有的对象return instance;}private Singleton(){//其他代码就没法new了}
}

在这个类中,我们创建出了唯一的对象,被static修饰,说明这个变量是类变量,(由类对象所拥有(每个类的类对象只存在一个),在类加载的时候,它就已经被初始化了)

而将构造方法设为私有,就使得只能在当前类里面创建对象了,其他位置就不能再创建对象了,因此这个instance指向的对象就是唯一的对象。

其他代码要想使用这个类的实例,就需要通过这个getInstance()方法获取这个对象,而无法在其他代码中new一个对象。

上述代码,称为”饿汉模式“,是单例模式中的一种简单的写法,”饿“形容”非常迫切“,实例在类加载的时候就创建了,创建的时机非常早,相当于程序一启动,实例就创建了。 

但是,上面的代码,面对反射,是无能为力的,也就是说,仍然可以通过反射来创建对象,但反射是属于非常规的编程手段,代码中随意使用反射是非常糟糕的。

2 懒汉模式

”懒“这个词,并不是贬义词,而是褒义词。社会能进步,科技能发展,生产效率提高,有很大部分原因都是因为懒。

举个生活中的例子(不考虑卫生):

假如我每次吃完饭就洗碗,那我每次就需要洗全部的碗;但是如果我每次吃完饭把碗放着,等到下次吃饭的时候再洗,此时,如果我只要用到两个碗,那我就只需要洗两个碗就行了,很明显洗两个碗要比洗全部碗更加高效。

在计算机中,”懒“的思想就非常有意思,它通常代表着更加高效

比如有一个非常大的文件(10GB),使用编辑器打开这个文件,如果是按照”饿汉“的方式 ,编辑器就会先把这10GB的数据都加载到内存中,然后再进行统一的展示。(但是加载了这么多数据,用户还是需要一点一点地看,没法一下子看完这么多)

如果是按照”懒汉“地方式,编辑器就会只读取一小部分数据(比如只读取10KB),把这10KB先展示出来,然后随着用户进行翻页之类的操作,再继续展示后面的数据。

加载10GB的时间会很长,但是加载10KB却只是一瞬间的事情……

懒汉模式,区别于饿汉模式,创建实例的时机不一样了,创建实例的时机会更晚,一直到第一次使用getInstance方法时才会创建实例。

代码如下(注意:这是一个不完整的代码,因为还有一些线程安全问题需要解决~~):

//懒汉的方式实现单例模式class SingletonLazy{private static SingletonLazy instance = null;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

第一行代码中仍然是先创建一个引用,但是这个引用不指向任何的对象。如果是首次调用getInstance方法,就会进入if条件,创建出对象并且让当前引用指向该对象。如果是后续调用getInstance方法,由于当前的instance已经不是null了,就会返回我们之前创建的引用了。

这样设定,仍然可以保证,该类的实例是唯一一个,与此同时,创建实例的时机就不再是程序驱动了,而是当第一次调用getInstance的时候,才会创建。。

而第一次调用getInstance这个操作的执行时机就不确定了,要看程序的实际需求,大概率会比饿汉这种方式要晚一些,甚至有可能整个程序压根用不到这个方法,也就把创建的操作给省下了。

有的程序,可能是根据一定的条件,来决定是否要进行某个操作,进一步来决定是否要创建实例。 

3 单例模式与线程安全

上面我们介绍的关于单例模式只是一个开始,接下来才是我们多线程的真正关键问题。即:上述我们编写的饿汉模式和懒汉模式,是否是线程安全的?

饿汉模式:

//饿汉模式
//期望这个类只能有唯一的实例(一个进程中)
class Singleton{private static Singleton instance = new Singleton();//在这个类被加载时,就会初始化这个静态成员,实例创建的时机非常早——饿汉public static Singleton getInstance(){//其他代码想要使用这个类的实例就需要通过这个方法进行获取,// 不应该在其他代码中重新new这个对象而是使用这个方法获取这个现有的对象return instance;}private Singleton(){//其他代码就没法new了}
}

对于饿汉模式来说,getInstance直接返回instance这个实例,这个操作,本质上就是一个的操作(多个线程同时读取同一变量,是不会产生线程安全问题的)。因此,在多线程下,它是线程安全的。

懒汉模式 :

//懒汉的方式实现单例模式class SingletonLazy{private static SingletonLazy instance = null;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

再看懒汉模式,在懒汉模式中,代码中有的操作(return instance),又有的操作(instance = new SingletonLazy())。 很明显,这是一个有线程安全问题的代码!!!

问题1:线程安全问题

因为多线程之间是随机调度,抢占是执行的,如果t1和 t2 按照下列的顺序执行代码,就会出现问题。

如果是t1和t2按照上述情况操作,就会导致实例被new了两次,这就不是单例模式了,就会出现bug了!!!

那如何解决当前的代码bug,使它变为一个线程安全的代码呢?

加锁~~

知道要加锁了?那大家不妨想想:如果我把锁像如下代码这样加下去,是否线程就安全了呢?

class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if (instance == null) {//如果首次调用就创建实例sychronized(locker){instance = new SingletonLazy();}}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

答案很显然:不行!!!因为如上述代码加锁仍然会发生刚才那样的线程不安全的情况。

所以这里如果想要代码正确执行,需要把if和new两个操作,打包成一个原子的操作(即加锁加在if语句的外面)。 

class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了synchronized(locker){    if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}  //不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

 

此时因为t1拿到了锁,t2进入阻塞,等t1执行完毕后(创建完对象后),t2进行判断,此时因为t1已经创建好了对象,所以t2就只能返回当前对象的引用了。 

多线程的代码是非常复杂的,代码稍微变化一点,结论就可能截然不同。千万不能认为,代码中加了锁就一定线程安全,不加锁就一定线程不安全,具体问题要具体分析,要分析这个代码在各种调度执行顺序下不同的情况,确保每种情况都不会出现bug!!!

 问题2:效率问题

上述代码还存在的另一个问题是效率问题:试想一下,当你创建完这个单例对象,你每次获取这个单例对象时(是读的操作,并不会有线程问题),每次都要去加锁、解锁,然后才能返回这个对象。(注意:加锁、解锁耗费的空间和时间都是很大的)。

所以为了优化上面的代码,我们可以再加上一层if,如果instance为null(需要执行写操作),考虑到线程安全问题,就需要加锁;如果instance不为null了,就不需要加锁了。

class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if(instance == null){synchronized(locker){    if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}}    //不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

上面的代码,有两重完全相同if判断条件,但是他们的作用是完全不同的:

第一个if是判断是否需要加锁,第二个if是判断是否要创建对象!!!

巧合的是,两个if条件相同,但是他们的作用是完全不同的,这样就实现了双重校验锁。在以后的学习中,还可能出现两个if条件是相反的情况。

问题3:指令重排序问题

这个代码还有一点问题需要解决:我们之前在线程安全的原因中讲过的:指令重排序问题就在懒汉模式上出现了~~

指令重排序,也是编译器优化的一种方式。编译器会在保证逻辑不变的前提下,为了提高程序的效率,调整原有代码的执行顺序。

再举个生活中的例子:

我妈让我去超市买东西:西红柿、鸡蛋、黄瓜、茄子。

超市摊位分布图如下:

如果我按我妈给的顺序,那就会走出这样的路线: 

上述方案虽然也能完成我妈给的任务,但如果我对超市已经足够熟悉了,我就能够在保证逻辑不变

的情况下(买到4种菜),调整原有买菜的执行顺序,提高买菜效率: 

返回到代码中:

   instance = new SingletonLazy();

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

1、申请一段内存空间。

2、调用构造方法,创建出当前实例。

3、把这个内存地址赋给instance这个引用。

上述代码可以按1、2、3这个顺序来执行,但是编译器也可能会优化成1、3、2这个顺序执行。这两种顺序在单线程下都是能够完成任务的。

1就相当于买了个房子

2相当于装修房子

3相当于拿到了房子的钥匙

通过1、2、3得到的房子,拿到的房子已经是装修好的,称为“精装房”;通过1、3、2得到的房子,拿到的房子需要自己装修,称为“毛坯房”,我们买房子时,上面的两种情况都可能发生。

但是,如果在多线程环境下,指令重排序就会引入新问题了。

上述代码中,由于 t1 线程执行完 1 3 步骤(申请一段内存空间,把内存空间的地址赋给引用变量,但并没有进行 2 调用构造方法的操作,会导致 instance指向的是一个未被初始化的对象)之后调度走,此时 instance 指向的是一个非 null 的,但是是未初始化的对象,此时 t2 线程判定 instance == null 不成立,就会直接 return,如果 t2 继续使用 instance 里面的属性或者方法,就会出现问题,引起代码的逻辑出现问题。 

那么我们应该如何解决当前问题呢?

volatile关键字

之前讲过volatile有两个功能:

1、保证内存可见性:每次访问变量都必须要重新读取内存,而不会优化为读寄存器/缓存。

2、禁止指令重排序:针对被volatile修饰的变量的读写操作的相关指令,是不能被重排序的。

懒汉模式的完整代码:

//经典面试题!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
package Thread;
//懒汉的方式实现单例模式
//线程不安全,它在多线程环境下可能会创建多个实例
class SingletonLazy{//这个引用指向唯一实例,这个引用先初始化为null,而不是立即创建实例
private volatile static SingletonLazy instance = null;//针对这个变量的读写操作就不能重排序了
private static Object locker;
//第一次if判定是否要加锁,第二次if判定是否要创建对象//双重校验锁public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了//加锁效率不高,且容易导致阻塞,所以再加一个判断提高效率if(instance ==null) {//判断是否为空,为空再加锁//不为空,说明是后续的调用就无需加锁了synchronized (locker) {if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

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

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

相关文章

【计算机组成原理】第二部分 存储器--分类、层次结构

文章目录 分类&层次结构0x01 分类按存储介质分类按存取方式分类按在计算机中的作用分类 0x02 层次结构 分类&层次结构 0x01 分类 按存储介质分类 半导体存储器磁表面存储器磁芯存储器光盘存储器 按存取方式分类 存取时间与物理地址无关(随机访问&#…

迅为RK3588开发板安卓GPIO调用APP运行测试

将网盘上的安卓工程文件复制到 Windows 电脑上。确保工程路径中使用英文字符,不包含中文。接着,启动 Android Studio,点击“Open”按钮选择应用工程文件夹,然后点击“OK”。由于下载 Gradle 和各种 Jar 包可能需要一段时间&#x…

BFS算法篇——打开智慧之门,BFS算法在拓扑排序中的诗意探索(下)

文章目录 引言一、课程表1.1 题目链接:https://leetcode.cn/problems/course-schedule/description/1.2 题目分析:1.3 思路讲解:1.4 代码实现: 二、课程表||2.1 题目链接:https://leetcode.cn/problems/course-schedul…

计数循环java

import java.util.Scanner;public class Hello {public static void main(String[] args) {Scanner in new Scanner(System.in);int count 10;while(count > 0) {count count -1;System.out.println(count);}System.out.println(count);System.out.println("发射&am…

11. CSS从基础样式到盒模型与形状绘制

在前端开发中,CSS(层叠样式表)是控制网页样式和布局的核心技术。整理了关于 CSS 基础样式、文本样式、盒模型以及形状绘制的一些心得。以下是详细的学习笔记。 一、基础样式设置 1. 字体样式 字体样式是网页视觉呈现的重要组成部分&#xf…

双种群进化算法:动态约束处理与资源分配解决约束多目标优化问题

双种群进化算法:动态约束处理与资源分配解决约束多目标优化问题 一、引言 约束多目标优化问题(CMOPs)在工程设计、资源分配等领域广泛存在,其核心是在满足多个约束条件的同时优化多个目标函数。传统方法往往难以平衡约束满足与目…

【Qt】pro工程文件转CMakeLists文件

1、简述 Qt6以后默认使用cmake来管理工程,之前已经一直习惯使用pro,pro的语法确实很简单、方便。 很多项目都是cmake来管理,将它们加入到Qt项目中,cmake确实是大势所趋。比如,最近将要开发的ROS项目,也是使用的cmake语法。 以前总结的一些Qt代码,已经编写成pro、pri等…

手机换地方ip地址会变化吗?深入解析

在移动互联网时代,我们经常带着手机穿梭于不同地点,无论是出差旅行还是日常通勤。许多用户都好奇:当手机更换使用地点时,IP地址会随之改变吗?本文将深入解析手机IP地址的变化机制,帮助您全面了解这一常见但…

【Canda】常用命令+虚拟环境创建到选择

目录 一、conda常用命令 二、conda 环境 2.1 创建虚拟环境 2.2 conda环境切换 2.3 查看conda环境 2.4 删除某个conda环境 2.5 克隆环境 三、依赖包管理 3.1 安装命令 3.2 更新包 3.3 卸载包 3.4 查看环境中所有包 3.5 查看某个包的版本信息 3.6 搜索包 四、环境…

目标检测任务常用脚本1——将YOLO格式的数据集转换成VOC格式的数据集

在目标检测任务中,不同框架使用的标注格式各不相同。常见的框架中,YOLO 使用 .txt 文件进行标注,而 PASCAL VOC 则使用 .xml 文件。如果你需要将一个 YOLO 格式的数据集转换为 VOC 格式以便适配其他模型,本文提供了一个结构清晰、…

Python作业练习2

任务简述 if_name__main_的含义,why? 问题解答 在Python中,if __name__ __main__:是一种常见的惯用法,用于检查当前模块是否是主程序入口点。要理解其含义和用途,首先需要了解两个概念: 1. __name__: 这是一个特…

ppy/osu构建

下载 .NET (Linux、macOS 和 Windows) | .NET dotnet还行 构建:f5 运行:dotnet run --project osu.Desktop -c Debug

NY182NY183美光固态颗粒NY186NY188

NY182NY183美光固态颗粒NY186NY188 在存储技术的竞技场上,美光科技(Micron)始终扮演着革新者的角色。其NY系列固态颗粒凭借前沿的3D NAND架构和精准的工艺控制,成为企业级存储和数据中心的关键支柱。本文将围绕NY182、NY183、NY1…

C++的历史与发展

目录 一、C 的诞生与早期发展 (一)C 语言的兴起与局限 (二)C 的雏形:C with Classes (三)C 命名与早期特性丰富 二、C 的主要发展历程 (一)1985 年:经典…

DedeCMS-Develop-5.8.1.13-referer命令注入研究分析 CVE-2024-0002

本次文章给大家带来代码审计漏洞挖掘的思路,从已知可控变量出发或从函数功能可能照成的隐患出发,追踪参数调用及过滤。最终完成代码的隐患漏洞利用过程。 代码审计挖掘思路 首先flink.php文件的代码执行逻辑,可以使用php的调试功能辅助审计 …

计算机网络|| 常用网络命令的作用及工作原理

1.hostname 作用:显示计算机的完整计算机名的主机名部分。仅当 Internet 协议 (TCP/IP) 协议作为组件安装在网络的网络适配器的属性中时,此命令才可用。 2.ping 作用: 1.用来检测网络的连通情况和分析网络速度 2.根据域名得到服务器 IP …

用户态到内核态:Linux信号传递的九重门(二)

1. 保存信号 1.1. 信号其他相关常见概念 实际执⾏信号的处理动作称为信号递达(Delivery)。 信号从产⽣到递达之间的状态,称为信号未决(Pending)。 进程可以选择阻塞 (Block )某个信号。 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作。 1.…

tar -zxvf jdk-8u212-linux-x64.tar.gz -C /opt/module/这个代码的解释

tar -zxvf jdk-8u212-linux-x64.tar.gz -C /opt/module/ 这条命令的解释如下: 1. tar:这是 Linux 系统中用于归档和压缩文件的命令行工具。 2. -z:表示通过 gzip 压缩格式来处理文件,因为文件 jdk-8u212-linux-x64.tar.gz 是一个经…

SysAid On-Prem XML注入漏洞复现(CVE-2025-2776)

免责申明: 本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 前…

Nginx的增强与可视化!OpenResty Manager - 现代化UI+高性能反向代理+安全防护

以下是对OpenResty Manager的简要介绍: OpenResty Manager (Nginx 增强版),是一款容易使用、功能强大且美观的反向代理工具 ,可以作为OpenResty Edge 的开源替代品基于 OpenResty 开发,支持并继承 OpenRes…