<设计模式>单例模式懒汉和饿汉

news2024/11/29 12:52:40

目录

一、单例模式概述

二、懒汉模式和饿汉模式

1.饿汉模式

1.1代码实现

1.2实现细节

1.3模式优劣

2.懒汉模式

2.1代码实现

2.2实现细节

2.3模式优劣

三、多线程下的线程安全问题

1.懒汉和饿汉线程安全问题分析

1.1安全的饿汉模式

1.2不安全的懒汉模式

2.懒汉线程安全实现

2.1代码实现

2.2实现细节


一、单例模式概述

单例模式是一种创建型模式,它的目的是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。单例模式通常用于需要频繁创建和销毁同一对象的场景,通过单例模式可以减少系统性能开销,提高系统性能。

基础的单例模式分为两种,懒汉模式和饿汉模式。

例如对于相当大的对象(假设其管理10G的数据),使用一次创建一次或是过多创建该对象都会造成较大的系统性能开销,那我们能不能规定整个这个类只能创建出一个实例,即每次创建实例返回的都是同一个对象,这样不但避免了使用一次就创建一次的性能开销,也能避免创建多个对象对空间资源的浪费。

可以结合下例进一步理解:

 对于一个“自助调料区”对象,在火锅店中需要他的“客人”线程可以在任何时间任何地区,同时同地的前来操作“自助调料区”对象,获取需要的内容。

在这一场景下,多个“自助调料区”对象无疑是没有必要的,正是单例模式大显身手的地方,采用了单例模式的火锅店就像是下达了“禁止多设调料区,需要调料都到这一个调料区”的指令,避免了资源的浪费。

说了这么多,单例模式怎么使用,又是怎么实现的呢?

且看下文。

二、懒汉模式和饿汉模式

1.饿汉模式

饿汉模式是单例模式的一种简单实现,‘式如其名’,饿汉模式主打的就是一个饿死鬼投胎,即类加载阶段就已经把实例创建出来了,相当于程序已启动就有这个实例了,总之非常迫切的感觉。

在饿汉模式中,类加载的时候就已经实例化对象,即“饿汉”在类加载时就完成了初始化,因此可以保证只有一个实例存在。

1.1代码实现

虽然Java标准库没法直接规定类所能创建实例的数量,但我们依然可以通过一些方法限制实例的创建,间接达到只能创建一个实例的效果。

饿汉模式的代码具体实现如下:

class Singleton {
    //实例为static修饰的类属性,类加载阶段创建
    private static Singleton instance = new Singleton();
    //通过getInstance方法获取唯一实例
    public static Singleton getInstance() {
        return instance;
    }
    //私有构造方法,无法通过new创建该类实例
    private Singleton() {};
}
1.2实现细节

第二行代码“private static Singleton instance = new Singleton();”

instance变量是Singleton类创建的唯一实例,分别被‘private’关键字和‘static’关键字修饰。private保证了该变量为类所私有,外界无法直接访问和修改,只能通过下面的getInstance方法获取该实例。static表示类属性,即instance作为Singleton类的属性在类加载阶段就被创建出来,且具有唯一性。

第三~五行代码“getInstance()”

作为获取唯一实例的唯一方法存在,需要由public和static修饰,使外界可以通过类直接调用该方法。

第六行代码“private Singleton() {};”

private关键字使Singleton类的构造方法私有,这样外界就没法new该类了。

1.3模式优劣

这种方式实现简单,但会导致类加载时就创建对象,如果不需要使用该对象则会造成资源浪费。同时,由于实例化对象在类加载时完成,因此无法在运行时改变实例状态。

2.懒汉模式

不同于饿汉模式的急不可耐,懒汉模式采用的是摆烂策略,就像博主暑假在家一样,不喊我我就绝不出门,妥妥的宅男。

在懒汉模式中,类加载的时候不会实例化对象,而是在第一次调用getInstance方法时才实例化对象。

2.1代码实现
class SingletonLazy {
    private static SingletonLazy instance = null;
    //通过getInstance方法获取唯一实例
    public static SingletonLazy getInstance() {
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
    //私有构造方法,无法通过new创建该类实例
    private SingletonLazy() {};
}
2.2实现细节

上述实现和饿汉模式的实现差别不大,只不过并没有在类加载阶段直接创建实例,而是在第一次调用getIntance方法时才创建出实例,即调用getIntance且instance=null未初始化时。

2.3模式优劣

只有在需要时才创建对象,节省了系统资源,只是实现上要比饿汉模式要更加复杂。由于在多线程环境下可能导致多个线程同时实例化对象,因此需要加锁来保证线程安全。

三、多线程下的线程安全问题

上述懒汉模式和饿汉模式的实现针对的是单线程情况的代码,多线程下代码实现是否会出现问题还需要具体分析。

1.懒汉和饿汉线程安全问题分析

1.1安全的饿汉模式

我们知道,产生线程安全的原因可能是内存可见性、锁竞争、优化策略和线程调度策略,及其它。具体产生问题的原因可能是多个线程对同一空间读写操作产生的。

再看饿汉模式的代码实现

很显然Singleton类中唯一的可调用方法getIntance只涉及到读操作,并不会产生线程安全问题。而由于不涉及到锁,更不会因为锁竞争陷入死锁。所以,饿汉模式是线程安全的。

1.2不安全的懒汉模式

再看懒汉模式,getIntance方法中不仅涉及了读操作同时也涉及了写操作,这就为线程安全问题的产生埋下了隐患。

由于线程调度的随机性,当两个线程在同一时间调用该方法时,错落的执行顺序可能导致if语句出现不可避免的错判,进而导致最终创建了两个SingletonLazy实例,如下图:

①T1线程执行完if语句,因为第一次调用getIntance方法,intance==null,所以T1线程接下来将要创建SingletonLazy实例,并将其赋值给intance。

②轮到T2线程执行,由于T1线程中尚未进行实例创建,此时仍旧是instance==null,所以if语句判断通过。接下来创建实例、赋值一气呵成,最后还将创建的Singleton对象返回。

③再次轮到T1线程,继续执行,创建了一个和T2线程不同的Singleton实例,引用赋给instance。最后,这个引用又被返回。

综上所述,在多线程情况下竟然出现了两个懒汉实例,这不符合单例模式下一个类只能创建一个实例的原则,很可能产生无法预估的错误,妥妥的bug代码。所以单线程下实现的懒汉模式不是线程安全的。

2.懒汉线程安全实现

上文分析懒汉模式代码线程不安全的原因是进程的随即调度问题,这一点我们可以通过引入锁来保证代码的原子性(一个整体)。同时,还要注意其他线程安全问题。

2.1代码实现
class SingletonLazy {
    //锁对象
    private static Object lock = new Object();
    //唯一实例,新增的volatile关键字是为了禁止指令重排序导致bug
    private static volatile SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        //当instance不为空时不进行加锁,提高代码效率
        if(instance == null) {
            //通过锁保证创建实例代码的原子性,不会因为线程的随即调度而产生多个实例
            synchronized(lock) {
                //多线程情况下可能由于锁竞争陷入阻塞,所以其他线程可能创建过实例了
                if(instance == null){
                    //虽然new SingletonLazy可以分解为三个指令,
                    // 但instance受volatile保护不会指令重排序
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() {};
}
2.2实现细节

通过锁保证代码块的原子性,进而克服系统随机调度的问题:

①T1线程执行。实例未创建,if判断通过。

②轮到T2线程。实例未创建,if判断通过。T2线程首先获取到锁资源,synchronized代码块执行完毕后才释放锁。if判断通过,实例创建成功后,锁释放。

③锁释放,T1线程解除阻塞,获取到锁资源。由于T2线程已经创建实例成功,if判断不通过,不创建新的实例,解锁,返回instance。

④轮到T2线程,返回instance的值。

上述过程返回的是同一个实例,成功克服系统随机调度的问题。

通过额外的if嵌套提高代码效率:

对于单线程代码来说,两个完全一样的if判断就是在脱裤子放屁,多此一举。但对于多线程代码来说,由于线程的随机调度,线程阻塞等问题,紧邻的两行代码执行时间相隔的可能是海枯石烂。

就我们的代码来说,外层if的作用是在实例已经创建的情况下,如果再调用本方法,只需经过该if语句就可以直接返回值,结束方法。像较于加锁解锁,if作为跳转语句效率相对非常之高,可以提高代码的运行效率。

而内层if则是判断线程阻塞时其他线程没有创建实例,确保只创建出一个实例。

通过volatile关键字保障创建操作不会因为代码优化(指令重排序)产生问题:

指令重排序是因为编译器会在保持“代码逻辑不发生变化”这一前提下对我们的代码进行优化,举个形象的例子:

1.去楼下超市买菜

2.回家

3.下楼倒垃圾

假如我们的代码执行逻辑为1->2->3,代码优化过后可能执行逻辑就变为1->3->2,两种执行逻辑效果相同,但效率却大大提高了。

而在多线程代码中,代码优化却可能会导致bug的出现。例如当线程频繁对同一个变量进行读值,在代码优化过后可能就不会再从主内存中读值,而是直接从线程的寄存器中读值,这时如果修改主内存的值,线程是感知不到的,从而导致线程安全问题的出现。

new Singleton()可以分解为以下三个指令:

1.申请一段内存空间

2.在这个内存上调用构造方法,创造出这个实例

3.把这个内存地址赋值给instance引用变量

指令重排序会在保持“代码逻辑不发生变化”这一前提下对我们的代码进行优化。对于逻辑而言,上述三个指令的顺序123和132都是没有区别的,因此执行顺序可能被优化成132。

程序执行是一条指令一条指令执行的,因此三条指令执行过程中线程可能就会被调度走了,如下:

①执行完毕后instance不为空,但引用指向的空间还未初始化,因为指令2还未执行。

②因为instance不为空,外层if判断未通过,返回未初始化空间的引用instance,方法结束。因为实列未初始化,而初始化的时间又无法确定(随机调度,T1要和其他线程竞争),这时候使用这个实例就可能产生问题。

③执行时间不确定,可能产生问题。

想要避免上述情况的出现,就必须保证指令的执行顺序保持不变为123,想达到这一效果可以使用volatile关键字修饰instance,利用其禁止指令重排序的特性。

博主是Java新人,每位同志的支持都会给博主莫大的动力,如果有任何疑问,或者发现了任何错误,都欢迎大家在评论区交流“ψ(`∇´)ψ

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

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

相关文章

牛客周赛 Round 31

D. 思路&#xff1a;使用map构造两个链表。 #include <bits/stdc.h> using namespace std;map<int,int> l,r; int main() {int q;cin>>q;int op-1e9-1;int ed1e91;r[op]ed;l[ed]op;while(q--){int a;cin>>a;if(a1){int x,y;cin>>x>>y;int…

SpringBoot:实例一

一、实现的效果 在浏览器地址栏输入http://localhost:8080/hello&#xff0c;当前页面显示hello world 实例一代码&#xff1a;点击查看LearnSpringBoot01 点击查看更多的SpringBoot教程 二、 效果图 三、 pom.xml代码 <?xml version"1.0" encoding"UTF-…

JS(react)图片压缩+图片上传

上传dome var fileNodeTakeStock: any createRef();<inputref{fileNodeTakeStock}onChange{showPictureTakeStock}style{{ display: "none" }}id"fileInpBtn"type"file"accept"image/*" //限制上传格式multiple{false}capture&qu…

JS 基本语句

函数调用&#xff0c;分支&#xff0c;循环&#xff0c;语句示例。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"&g…

20240203在WIN10下安装Anaconda

20240203在WIN10下安装Anaconda 2024/2/3 20:45 缘起&#xff1a;最近学习stable-diffusion-webui.git&#xff0c;在Ubuntu20.04.6下配置SD成功。 不搞精简版本&#xff1a;Miniconda了。直接上Anacoda&#xff01; 百度搜索&#xff1a;Anaconda 下载 https://www.anaconda.c…

Django的web框架Django Rest_Framework精讲(一)

文章目录 Django Rest_Framework1. DRF介绍2.DRF特点3.环境安装与配置&#xff08;1&#xff09;DRF需要以下依赖&#xff08;2&#xff09;创建django项目 4.序列化器的使用&#xff08;1&#xff09;创建序列化器 5. 反序列化器使用 Django Rest_Framework 1. DRF介绍 Djan…

特种作业题不会怎么搜答案?推荐你使用这六个公众号和工具 #其他#学习方法特种作业题不会怎么搜答案?推荐你使用这六个公众号和工具 #其他#学习方法

作为当代大学生&#xff0c;我们常常面临着繁重的学业压力和众多的学习任务。在这个信息爆炸的时代&#xff0c;如何高效地进行搜题和学习成了我们迫切需要解决的问题。幸运的是&#xff0c;随着科技的不断进步&#xff0c;我们拥有了许多方便、实用的日常搜题和学习软件。 1.…

SpringBoot 过滤器Filter 拦截请求 生命周期

介绍 当用户请求接口时&#xff0c;请求会先到过滤器&#xff0c;在到处理逻辑的接口&#xff0c;在过滤器中可以可以判断用户权限&#xff0c;如&#xff1a;是否登录&#xff0c;或请求前的一些操作&#xff0c;完成一下比较通用的操作&#xff0c;如&#xff1a;前端的AXIO…

GNSS模块的惯导技术:引领定位科技的前沿

全球导航卫星系统&#xff08;GNSS&#xff09;模块的惯导技术是一项颇具前瞻性的科技&#xff0c;它结合了全球定位系统和惯性导航技术&#xff0c;为各个领域的定位需求提供了更为精准和可靠的解决方案。本文将深入探讨GNSS模块的惯导技术&#xff0c;以及它如何在多个领域中…

[word] word页面视图放大后,影响打印吗? #笔记#学习方法

word页面视图放大后&#xff0c;影响打印吗&#xff1f; word文档的页面视图又叫普通视图&#xff0c;又叫打印视图&#xff0c;是系统默认的视图&#xff0c;是用户用的最多最常见的视图。 问&#xff1a;怎样打开页面视图&#xff1f; 答&#xff1a;两种方法 方法一、点…

14:中断

中断 1、中断的引入2、使用单片机外部中断来处理按键2.1、外部中断2.2、参考数据手册中示例代码写程序2.2.1、外部中断0的测试程序2.2.2、完整程序 1、中断的引入 任务&#xff1a;独立数码管循环显示0-9&#xff0c;同时按键控制LED1亮灭。 代码如下&#xff1a; #include …

笔记---求组合数

总&#xff1a; ①10w次询问——1 < b < a < 2000——递推求—— N N N2 ②1w次询问——1 < b < a < 105——公式求—— N l o g N NlogN NlogN ③20次询问——1 < b < a < 1018——Lucas定理 ④1次询问——1 < b < a < 5000——拆分质因子…

ensp实验合集(一)

实验1 双绞线制作...................................................................... - 1 - 实验2 光纤熔接.......................................................................... - 3 - 实验3 eNSP模拟软件使用.........................................…

数据分析:当当网书籍数据可视化分析

当当网书籍数据可视化分析 作者&#xff1a;i阿极 作者简介&#xff1a;Python领域新星作者、多项比赛获奖者&#xff1a;博主个人首页 &#x1f60a;&#x1f60a;&#x1f60a;如果觉得文章不错或能帮助到你学习&#xff0c;可以点赞&#x1f44d;收藏&#x1f4c1;评论&…

面试经典150题——文本左右对齐(困难)

​"It always seems impossible until it’s done." - Nelson Mandela 1. 题目描述&#xff1a; 这个题目标为困难题目&#xff0c;但是如果我们静下心来把题目读懂了&#xff0c;其实无非就是不同情况下不同考虑而已&#xff0c;也没什么思维上的复杂&#xff0c;还…

Ainx框架实现 一

&#x1f4d5;作者简介&#xff1a; 过去日记&#xff0c;致力于Java、GoLang,Rust等多种编程语言&#xff0c;热爱技术&#xff0c;喜欢游戏的博主。 &#x1f4d7;本文收录于Ainx系列&#xff0c;大家有兴趣的可以看一看 &#x1f4d8;相关专栏Rust初阶教程、go语言基础系列…

对话 Zeitgeist|巧借波卡生态优势,开启去中心化预测市场新篇章

在纷繁芜杂的信息世界里&#xff0c;真相和事实往往是稀缺品&#xff0c;利用预测市场的激励方式往往能够带领我们前瞻未来发展的趋势&#xff0c;走向获得真理和进步的 Web3 新世界。—— Zeitgeist 团队 试想下&#xff0c;如果有一天&#xff0c;你可以自由地在感兴趣的行业…

【经典项目】Java小游戏 —— 会说话的汤姆猫

一、游戏回顾 【预期效果】 【玩法介绍】 1、 和它说话&#xff0c;它将用有趣的声音重复你的话。 2、打它的头&#xff0c;它会装成被打的样子&#xff0c;连续打还会晕倒&#xff1b;抚摸肚子&#xff0c;它会打呼噜&#xff1b;打肚子&#xff0c;它会装肚子疼&#xff1b…

QUIC with CUBIC or BBR

拥塞控制 拥塞控制算法是 TCP/QUIC 协议的一个基础部分&#xff0c;多年来经过一个个版本的迭代&#xff08;如 Tahoe、Reno、Vegas 等&#xff09;&#xff0c;拥塞控制算法得到了持续的提升。由于篇幅有限&#xff0c;本文就目前比较流行的两种拥塞控制算法&#xff08;CUBI…

[AIGC] 21世纪Java与Go的相爱相杀

在21世纪的软件开发领域中&#xff0c;Java和Go这两门编程语言可谓是相爱相杀的存在。它们各自拥有着强大的特点和独特的优势&#xff0c;同时也存在着一些明显的竞争和冲突。让我们来看看这两门语言的故事&#xff0c;以及它们之间的深远意义。 文章目录 Java的魅力Go的魅力相…