面试官:写一个单例模式

news2024/11/20 12:35:14

1. 什么是单例模式

了解单例模式之前,我们需要先了解什么是设计模式。

设计模式是一种抽象的编程思想,不局限于编程语言,简单来说,就是一些大佬程序猿针对一些典型的场景,给出一些典型的解决方案,只要按照这个方案来,也就是按照给定的设计模式,此时代码写的差,也差不到哪去。

本节讲解的就是设计模式中的其中一种:单例模式

设计模式本质就是一些规章制度,就比如变量命名的风格,如果这个模块要求使用某某设计模式,可以不用吗?当然可以,大不了被队友喷呗,只要内心足够强大。

让我印象特别深的一件事,我还在上学的时候,班里有个人觉得自己写的代码很厉害,往班级群里发,我一看那代码,我靠,shujuyuan,lianbiao,kehuduanqingqiu,这都什么变量名啊,代码能跑吗?能跑,这如果跟我做同事,我指定得喷他。

单例模式很好理解,单例单例,只能有单个实例嘛,就是一个类只能有实例一个对象嘛,不要理解成这个类只能 new 一次哈,而是这个类压根就不让你 new,但是会提供一个给你获取对象的方法,每次获取都是相同的对象!

单例模式的实现常见的有两种实现方式,分别是饿汉模式,懒汉模式,名字奇怪不要紧,后续会讲解。


2. 饿汉模式的实现

public class SingleHungry {
    // 提前创建好对象
    private static final SingleHungry instance = new SingleHungry();

    // 对外隐藏构造方法
    private SingleHungry() {

    }

    // 对外提供获取这个对象的方法
    public static SingleHungry getInstance() {
        return instance;
    }
}

此时饿汉版本的单例模式就实现完成了,阅读上述代码,我们已经提前创建好了 SingleHungry 的实例对象 instance 了,而且使用 private 关键字修饰了构造方法,也就是对外不提供构造方法了,紧接着又写了一个获取 instance 的方法,这样一来,每次获取到的实例都是同一个实例!

public class ThreadDemo {
    public static void main(String[] args) {
        SingleHungry instance1 = SingleHungry.getInstance();
        SingleHungry instance2 = SingleHungry.getInstance();
        System.out.println(instance1.equals(instance2));
    }
}
// 打印结果:true

当然如果你尝试用 SingleHungry instance = new SingleHungry();

这样显然是会报错的。

为什么这种方式叫做饿汉呢?由于 SingleHungry 类里面的 instance 变量是被 static 修饰的,表示是类的属性,所有对象共享的,只存在一份,并且这个 instance 是在类加载的时候被创建的,类加载是比较靠前的阶段,给人的感觉就是很着急,饿得慌,所以就叫做饿汉模式。


3. 懒汉模式的实现

懒汉模式则于饿汉模式相反,饿汉不是很着急吗,老早就把对象创建了,而懒汉模式则是你什么时候需要,我再创建,那么很多小伙伴想到了,这不是很简单吗?直接在 getInstance() 加一个判断嘛,具体代码实现如下:

public class SingleLazy {
    // 不提前创建好对象
    private static SingleLazy instance = null;

    // 对外隐藏构造方法
    private SingleLazy() {

    }

    // 对外提供获取这个对象的方法
    public static SingleLazy getSingleLazy() {
        if (instance == null) {
            instance = new SingleLazy();
        }
        return instance;
    }
}

那么对于懒汉模式来说,什么时候调用 SingleLazy.getSingleLazy() 的时候才会创建 SingleLazy 对象,效率显然是比饿汉模式要高的。


4. 多线程使用单例模式

大家可千万不要忘了,咱们的标题可还是多线程呐,如果单例模式仅仅是在单线程,那执行顺序都是唯一的,显然不会出现线程安全的问题,但是由于多线程环境下的随机调度,抢占式执行,这样一来,可能就会出问题。

此时来判断上述饿汉模式和懒汉模式在多线程环境下是否会出现线程安全的问题呢?

这里我们主要来看这两个版本里最主要的 get 方法的实现:

// 饿汉模式
public static SingleHungry getInstance() {
    return instance;
}

// 懒汉模式
public static SingleLazy getInstance() {
    if (instance == null) {
        instance = new SingleLazy();
    }
    return instance;
}

此时很明显发现,饿汉模式的实现,只要调用 get 方法,就会直接返回,不涉及对 instance 的修改,只涉及读操作,而懒汉模式中涉及到 load,cmp,new,save等,简直就是又有读和写啊。

前面讲线程安全时,涉及到读和写,都有可能出现线程不安全。

就懒汉模式的 getInstance 方法,可以大致分为四个步骤,先读取 instance 的值(load),于 null 做比较(cmp) 条件满足,进行 new 操作(new 也分为好几个步骤),接着在写回 instance(save)。

下面我们就画图模拟两个线程里都调用懒汉模式 getInstance 方法的情况:

上述情况之所以能触发多次 new 操作,本质上是因为比较,读,写这三个操作不是原子的,于是我们就可以通过加锁针对上述 getSingleLazy 方法做优化,保证原子性。

// 懒汉模式
public static SingleLazy getInstance() {
    synchronized (SingleLazy.class) {
        if (instance == null) {
            instance = new SingleLazy();
        }
    }
    return instance;
}

此时我们是解决了原子性的问题,但是这个代码会不会效率很低呢?

每次调用 getSingleLazy 方法的时候,都需要先加锁!加锁操作也是有开销的,那我们有必要每次调用该方法都加锁吗?

其实不用每次都加锁,当 instance 为 null 的时候,需要 new 对象的时候,才需要加锁,只要第一次 new 过对象了,后续都不需要加锁了,因为这个类只能实例一个对象,于是我们就可以在前面再次加上判断语句:

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

写到这里,我们还得思考,这个代码是否有内存可见性的问题,假设同一时间很多线程里面都调用了 getSingleLazy 方法,此时只能有一个线程在进行 new 操作,其他线程都在等待锁释放,那么那么多线程读 instance 发现都是 null。

当锁释放,另一个线程获取到锁,前面发现第一次 if 里面读的 instance 为 null,进入第二个 if 的时候,就没有从内存中再次读 instance,而是直接去 寄存器/cache 里读,所以仍然判断 instance 为 null,也会出现重复 new 对象的操作。

再者,instance = new SingleLazy(); 这个操作可以大致拆分成三个步骤:

1. 申请内存空间
2. 调用对应构造方法,初始化内存空间
3. 把内存空间的地址赋值给 instance 引用

如果编译器优化,指令重排序了,本来是按照 1 2 3 的顺序,如果优化成了 1 3 2 的顺序,在多线程的情况,1 2 3 顺序和 1 3 2 是一样的结果,但是多线程环境可不一定了!

假设 t1 线程按照 1 3 2 的顺序去执行,执行完 3 这个操作后,此时 instance 里只是存了一个地址,但地址对应的内存空间并没有初始化,此时如果 t1 被 CPU 切走了(CPU调用其他线程了),CPU 开始调用 t2 线程执行 1 2 3 顺序,此时 t2 就发现 instance 里不为 null,直接 return instance; 但是这个 instance 对应的内存空间并未初始化,所以 t2 调用 getInstance() 得到的就是一个非法的对象(未初始化的对象)。

如上可知,我们要避免出现内存可见性和指令重排序的问题,这时就可以用我们前面学到过的 volatile 关键字,来保证内存可见性,禁止指令重排序。

最终完整版的懒汉模式代码如下:

public class SingleLazy {
    // 不提前创建好对象
    private volatile static SingleLazy instance = null;

    // 对外隐藏构造方法
    private SingleLazy() {

    }

    // 对外提供获取这个对象的方法
    public static SingleLazy getSingleLazy() {
        if (instance == null) {
            synchronized (SingleLazy.class) {
                if (instance == null) {
                    instance = new SingleLazy();
                }
            }
        }
        return instance;
    }
}

当然这个代码还不完整!虽然我们构造方法是私有的,但是利用反射仍然可以构造多个对象,这里可以采用枚举来防止被反射,还有防止序列化的情况,但是反射是非常规编程手段,如果被面试官要求写个懒汉模式,其实写到这,已经够了!


下期预告:【多线程】生产者消费者模型

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

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

相关文章

04-CSS3-渐变色、2D转换、3D转换

一、渐变色 CSS渐变色(Gradient)是指在元素背景中使用两种或多种不同的颜色进行过渡,超过两个颜色可以形成更为细腻的渐变效果。常见的CSS渐变色有线性渐变和径向渐变。 1. 线性渐变:Linear Gradients 向下/向上/向左/向右/对角…

SVN 修改URL路径-使用重新定位(relocate)命令和找不到问题解决

当svn服务器url发生变更,又不想在本地重新进行checkout操作,这时候可以使用svn relocate命令进行url的重新定位; 在windows下以TortoiseSVN为例,在仓库文件夹上右键,TortoiseSVN-(重新定位)relocate, 1、Windows TortoiseSVN客户端: 在工作复本的根目录上右键->TortoiseSV…

DOUBLETROUBLE: 1

文章目录 DOUBLETROUBLE: 1实战演练一、前期准备1、相关信息 二、信息收集1、nmap探测目标靶机端口2、扫描目标网址目录3、访问网站,发现secret下有个图片4、将图片下载5、查看图片所含内容6、破解密码并查看7、登陆邮箱8、创建反弹shell9、上传反弹shell10、监听11…

Jeecg-Boot 未授权SQL注入漏洞(CVE-2023-1454)

本文转载于:https://blog.csdn.net/qq_27536045/article/details/129944987 环境搭建 JDK: 1.8 (小于11) Maven: 3.5 MySql: 5.7 Redis: 3.2 Node Js: 10.0 Npm: 5.6.0 Yarn: 1.21.1 下载源码 后端源码 https://github.com/jeecgboot/jeecg-boot/tree/v…

MongoDB安装教程—Ubuntu

为啥用MongoDB,问就是客户要求。 为啥用Ubuntu,问就是客户只有Ubuntu的机器。 0. 环境 操作系统: Ubuntu 22.04.1 LTS (GNU/Linux 5.19.0-41-generic x86_64) 不同版本系统差异不同,其他版本系统未测试。 1. 安装 1.1 包管理公…

深入探索SDL游戏开发

前言 欢迎来到小K的SDL专栏第二小节,本节将为大家带来基本窗口构成、渲染器、基本图形绘制、贴图、事件处理等的详细讲解,看完后希望对你有收获 文章目录 前言一、简单窗口二、渲染器三、基本图形绘制1、点2、线3、矩形4、圆和椭圆 四、贴图五、事件处理…

XR交互技术趋势:6DoF追踪、手势识别、眼动跟踪……

XR交互技术提供了用户与虚拟环境进行交互的方式和手段,而实时云渲染则提供了真三维、可交互、高沉浸的图形渲染和计算能力。结合这两者,用户可以通过XR设备获得更真实、更沉浸的虚拟体验,同时享受到优质的图形效果和流畅的交互响应。本篇文章…

关于开发中对端口(port)的几点理解

一、服务端的端口是固定的,客户端的端口是随机的 客户端端口是随机的,比如访问百度,系统为浏览器分配了个端口1024。过一会重开电脑,访问了新浪,可能还是用1024端口,我不关浏览器,还要再开一个浏…

CenterFusion数据处理函数__getitem__()解析

CenterFusion数据处理函数__getitem__解析 1. 图像数据处理1.1 通过利用nuScence_COCO实例化对象获取图像以及相关数据的信息1.2 获取图像数据增强的相关参数:中心点c,尺度scale,旋转rotia和翻转flip1.3 根据生成的参数生成仿射矩阵来对图像进…

spring boot 集成 swagger3

Swagger 3是一种开源的API描述工具,它可以帮助开发人员设计、构建、文档化和测试API。Swagger 3支持多种编程语言和框架,包括Java、Node.js、Python、Ruby等,并提供了许多集成工具和插件,例如Postman、Apigee等。 Swagger 3使用Op…

北京君正应用案例:双镜头双画面乔安枪球联动摄像头

你是否遇到过这种问题? 既要看店铺又要看柜台 既要看车又要看大门 雷龙发展提供原厂技术支持,并提供君正集成电路完整解决方案,大大降低你的开发难度及开发时间。 单镜头摄像头一台不够广 出现监控盲区,让小偷有可趁之机 只能装两…

sql语句---left join or right join

1068. 产品销售分析 I 销售表 Sales: -------------------- | Column Name | Type | -------------------- | sale_id | int | | product_id | int | | year | int | | quantity | int | | price | int | -------------------- (s…

JS文字转语音技术实现

前言 最近在做排队叫号系统,涉及到文字转语音播报,因此总结了几种前端文字转语音发声的方法。 一、Web Speech API h5新提供的一个原生语音识别技术的API,可以将文本转成语音并播放。 作为官方的api,实现的效果是比较符合理想的…

Windows平台下用例图中包含(include)、扩展(extend)和泛化(generalization)介绍

我是荔园微风,作为一名在IT界整整25年的老兵,今天总结一下Windows平台下用例图中包含(include)、扩展(extend)和泛化(generalization)介绍。 用例图是解决用户需求的图,画好用例图一定要理清用例之间的关系。用例之间有三种关系&…

LabVIEWCompactRIO 开发指南33 测试和调试LabVIEW FPGA代码

LabVIEWCompactRIO 开发指南33 测试和调试LabVIEW FPGA代码 如前所述,应在仿真模式下开发LabVIEWFPGA VI,以快速迭代设计并避免冗长的编译时间。当需要测试和调试VI时,可以保持仿真模式或利用其他几个选项。应该根据功能验证与性能的要求以…

【LeetCode】 复制带随机指针的链表

Leetcode 138.复制带随机指针的链表 文章目录 题目描述解题思路运行代码 题目描述 给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全…

C语言中这么骚的退出程序方式你知道几个?

前言 在本篇文章当中主要给大家介绍C语言当中一些不常用的特性,比如在main函数之前和之后设置我们想要执行的函数,以及各种花式退出程序的方式。 1、main函数是最先执行和最后执行的函数吗? 1)C语言构造和析构函数 通常我们在…

Python数据分析案例27——PCA-K均值-轮廓系数客户聚类

本案例适合应用统计,数据科学,电商专业 K均值对客户进行分类的案例都做烂了......但我认为这个案例还是有一定的价值的,使用了pca,还有轮廓系数寻找最优的聚类个数。 下面来看看 代码准备 导入包 import numpy as np import pa…

网上学影视后期靠谱吗 影视后期剪辑需要学什么

影视后期如果有人手把手当面教的话,当然是最好的。但很多人都没有这么好的条件,实际上,网上也有很多教程可以学习利用。不过,小伙伴们可能会有疑问,网上学影视后期靠谱吗,影视后期剪辑需要学什么&#xff1…

从创意造型到高品质曲面的卓越体验|CATIA ICEM Design Experience

目录 IDX为设计师提供了强大直观的建模工具 IDX为曲面工程师提供高品质数字模型处理能力 IDX与其他工具配合形成完整的数字化解决方案 建模是设计工作的重要环节,合适的数字模型能够在各个环节对整个设计流程产生正面的推动作用。 设计的不同阶段对模型有各自的…