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

news2025/7/4 5:22:02

单例模式

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

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

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

举个例子:有时候,代码中需要管理/持有大量的数据,此时有一个对象就可以了。比如:我需要一个对象管理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.coloradmin.cn/o/2378049.html

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

相关文章

CSS实现文本自动平衡text-wrap: balance

不再有排版孤行和寡行 我们都知道那些标题,最后一个单词换行并单独站在新行上,破坏了视觉效果,看起来很奇怪。当然,有老式的 手动换行或将内容分成不同部分。但您听说过text-wrap: balance吗? 通过应用text-wrap: bal…

mac M芯片运行docker-desktop异常问题

虽然mac已经迭代到m4了,但官方的docker-desktop运行仍然有问题,包括但不限于: 命令行docker找不到docker-desk打不开docker-desktop闪退容器起不来 尝试不同版本后,看到了其他可以在mac跑docker的开源方法,更简单、轻…

事件响应策略规范模版

事件响应策略 一、事件分级定义 根据事件对业务的影响程度和紧急程度,将事件分为 4个等级(P1-P4),明确各级事件的判定标准:、 二、响应时效承诺 响应时间(从事件确认到首次回复) P1 事件:15 分钟内响应(724 小时电话 / 工单优先接入) P2 事件:30 分钟内响应(工…

MGX:多智能体管理开发流程

MGX的多智能体团队如何通过专家混合系统采用全新方法,彻底改变开发流程,与当前的单一智能体工具截然不同。 Lovable和Cursor在自动化我们的特定开发流程方面取得了巨大飞跃,但问题是它们仅解决软件开发的单一领域。 这就是MGX(MetaGPT X)的用武之地,它是一种正在重新定…

采购流程规范化如何实现?日事清流程自动化助力需求、采购、财务高效协作

采购审批流程全靠人推进,内耗严重,效率低下? 花重金上了OA,结果功能有局限、不灵活? 问题出在哪里?是我们的要求太多、太苛刻吗?NO! 流程名称: 采购审批管理 流程功能…

[模型部署] 3. 性能优化

👋 你好!这里有实用干货与深度分享✨✨ 若有帮助,欢迎:​ 👍 点赞 | ⭐ 收藏 | 💬 评论 | ➕ 关注 ,解锁更多精彩!​ 📁 收藏专栏即可第一时间获取最新推送🔔…

Vue3 加快页面加载速度 使用CDN外部库的加载 提升页面打开速度 服务器分发

介绍 CDN(内容分发网络)通过全球分布的边缘节点,让用户从最近的服务器获取资源,减少网络延迟,显著提升JS、CSS等静态文件的加载速度。公共库(如Vue、React、Axios)托管在CDN上,减少…

接触感知 钳位电路分析

以下是NG板接触感知电路的原理图。两极分别为P3和P4S&#xff0c;电压值P4S < P3。 电路结构分两部分&#xff0c;第一部分对输入电压进行分压钳位。后级电路使用LM113比较器芯片进行电压比较&#xff0c;输出ST接触感知信号。 钳位电路输出特性分析 输出电压变化趋势&a…

使用 gcloud CLI 自动化管理 Google Cloud 虚拟机

被操作的服务器&#xff0c;一定要开启API完全访问权限&#xff0c;你的电脑安装gcloud CLI前一定要先安装Python3&#xff01; 操作步骤 下载地址&#xff0c;安装大概需要十分钟&#xff1a;https://cloud.google.com/sdk/docs/install?hlzh-cn#windows 选择你需要的版本&a…

SQL语句,索引,视图,存储过程以及触发器

一、初识MySQL 1.数据库 按照数据结构来组织、存储和管理数据的仓库&#xff1b;是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合&#xff1b; 2.OLTP与OLAP OLTP&#xff08; On-Line transaction processing &#xff09;翻译为联机事务处理&am…

7. 进程控制-进程替换

目录 1. 进程替换 1.1 单进程版&#xff1a; 1.2 进程替换的原理 1.3 多进程版-验证各种程序替换接口 2. 进程替换的各种接口 2.1 execl 2.2 execlp 2.3 execv 2.4 execvp 2.5 execle 1. 进程替换 上图为程序替换的接口&#xff0c;之后会详细介绍。 1.1 单进程版&am…

理解 C# 中的各类指针

前言 变量可以理解成是一块内存位置的别名&#xff0c;访问变量也就是访问对应内存中的数据。 指针是一种特殊的变量&#xff0c;它存储了一个内存地址&#xff0c;这个内存地址代表了另一块内存的位置。 指针指向的可以是一个变量、一个数组元素、一个对象实例、一块非托管内存…

真题卷001——算法备赛

蓝桥杯2024年C/CB组国赛卷 1.合法密码 问题描述 小蓝正在开发自己的OJ网站。他要求用户的密码必须符合一下条件&#xff1a; 长度大于等于8小于等于16必须包含至少一个数字字符和至少一个符号字符 请计算一下字符串&#xff0c;有多少个子串可以当作合法密码。字符串为&am…

Vue3中实现轮播图

目录 1. 轮播图介绍 2. 实现轮播图 2.1 准备工作 1、准备至少三张图片&#xff0c;并将图片文件名改为数字123 2、搭好HTML的标签 3、写好按钮和图片标签 ​编辑 2.2 单向绑定图片 2.3 在按钮里使用方法 2.4 运行代码 3. 完整代码 1. 轮播图介绍 首先&#xff0c;什么是…

微信小程序 自定义图片分享-绘制数据图片以及信息文字

一 、需求 从数据库中读取头像&#xff0c;姓名电话等信息&#xff0c;当分享给女朋友时&#xff0c;每个信息不一样 二、实现方案 1、先将数据库中需要的头像姓名信息读取出来加载到data 数据项中 data:{firstName:, // 姓名img:, // 头像shareImage:,// 存储临时图片 } 2…

全栈项目中是否可以实现统一错误处理链?如果可以,这条链路该如何设计?需要哪些技术支撑?是否能同时满足性能、安全性和用户体验需求?

在复杂系统中&#xff0c;错误一旦出现&#xff0c;可能不断扩散&#xff0c;直到让整个系统宕机。尤其在一个全栈项目中&#xff0c;从数据库到服务器端逻辑、再到前端用户界面&#xff0c;错误可能在任意一个环节产生。如果我们不能在全栈范围内实现统一的错误处理机制&#…

排序01:多目标模型

用户-笔记的交互 对于每篇笔记&#xff0c;系统记录曝光次数、点击次数、点赞次数、收藏次数、转发次数。 点击率点击次数/曝光次数 点赞率点赞次数/点击次数 收藏率收藏次数/点击次数 转发率转发次数/点击次数 转发是相对较少的&#xff0c;但是非常重要&#xff0c;例如转发…

Dify中使用插件LocalAI配置模型供应商报错

服务器使用vllm运行大模型&#xff0c;今天在Dify中使用插件LocalAI配置模型供应商后&#xff0c;使用工作流的时候&#xff0c;报错&#xff1a;“Run failed: PluginInvokeError: {"args":{},"error_type":"ValueError","message":&…

初识计算机网络。计算机网络基本概念,分类,性能指标

初识计算机网络。计算机网络基本概念&#xff0c;分类&#xff0c;性能指标 本系列博客源自作者在大二期末复习计算机网络时所记录笔记&#xff0c;看的视频资料是B站湖科大教书匠的计算机网络微课堂&#xff0c;祝愿大家期末都能考一个好成绩&#xff01; 视频链接地址 一、…

C++ QT图片查看器

private:QList<QString> fs;int i;void MainWindow::on_btnSlt_clicked() {QStringList files QFileDialog::getOpenFileNames(this,"选择图片",".","Images(*.png *.jpg *.bmp)");qDebug()<<files;ui->picList->clear();ui-…