[单例模式]

news2024/11/13 4:19:22

[设计模式]

设计模式是软件工程中的一种常见做法, 它可以理解为"模板", 是针对一些常见的特定场景, 给出的一些比较好的固定的解决方案

不同语言适用的设计模式是不一样的. 这里我们接下来要谈到的是java中典型的设计模式. 而且由于设计模式比较适合有一定编程经验之后, 再去详细学习, 所以我们本篇文章就只讨论几个经典的java设计模式

  • 单例模式

在实际开发中, 某个进程中, 我们不希望某个类有多个实例对象, 希望它有且仅有一个实例对象而且不能再创建出来. --> 这个时候我们就可以使用单例模式这样的设计模式. 单例模式有两种写法, 一种叫饿汉模式, 一种叫懒汉模式. 下面我们就详细讨论一下这两种单例模式的写法.

1. 饿汉模式

"饿"的意思是"迫切的", 放到代码中意思就是需要我们在类被加载的时候就创建出这个单例的实例. 

class Singleton {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() { //获取Singleton的实例对象, 但是每次获取的都是相同的对象instance.
        return instance;
    }
    private Singleton() {} //单例模式中最关键的部分: 将构造方法设置为私有. 防止在类外再创建出其他实例对象.
}
public class Demo24 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //两次获取到的对象应该是同一个对象. 我们可以在下面验证一下 (== 比较的是两个对象在内存中的地址,也就是它们是否指向同一个实例对象)
        System.out.println(s1 == s2);
    }
}

 我们可以看到, 饿汉模式中,

(1) static修饰instance, 说明这里的instance是类成员(一个类只有一份, 随着类的加载而创建出来)

(2) static修饰的类方法 getInstance() 每次返回的都是同一个对象 instance.

(3) 将SIngleton类的构造方法设置为私有, 这就保证了在类外无法通过构造方法再创建出新的对象.

我们通过在main方法中创建两个引用s1和s2, 看到s1和s2指向的是同一个对象. 这也就代表了Singleton这个类只有一个实例. 

[注]: 上述单例模式只能避免程序员失误, 调用了Singleton的构造方法创建新对象;  而无法避免程序员故意破坏单例模式(比如, 我们可以通过反射的方式拿到构造方法).

2. 懒汉模式

我们先通过一个形象的例子来理解饿汉和懒汉的区别: 比如我们现在有一个编辑器, 要打开一个非常大的文本文档. (1) 饿汉: 一启动, 就把所有的文本内容全都读取到内存中, 然后显示到界面. (2) 懒汉: 先只加载出一部分数据, 随着用户的翻页操作, 再按需加载剩下的内容.  根据上述表述, 我们可以确定, 懒汉模式一定比饿汉模式加载出来的速度更快, 用户的体验也就会更好. 所以, 我们日常开发中, 很多地方都青睐于使用懒汉模式.

class SingletonLazy {
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
        // 如果instance为空,则创建一个实例对象; 如果instance不为空,则直接返回instance
    }
    private SingletonLazy() {}
}

public class Demo25 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

如上述代码, 当我们首次调用getInstance时, 由于此时对象还没有创建, instance这个引用为空, 所以就会进入if分支, 创建出SingletonLazy对象. 后续如果再重复调用getInstance, 结果都不会再创建新的实例, 而是直接返回instancomen

 我们还是通过在main方法中创建两个引用s1和s2, 看到s1和s2指向的是同一个对象. 这也就代表了SingletonLazy这个类只有一个实例.

3. 单例模式的线程安全问题

知道了单例模式的两种写法之后, 我们现在要判断: 这两种写法是否存在线程安全问题呢? (在多线程环境下, 多个线程调用getInstance, 是否会出现问题?)

(1) 饿汉模式:

我们可以看到, 饿汉模式的getInstance方法只涉及读操作, 并没有涉及任何写操作. 而我们在多个线程同时修改同一个变量时, 才容易出现线程安全问题. 所以饿汉模式是线程安全的.

(2) 懒汉模式:

  • 原子操作问题

像这种  先条件判定, 再修改 的操作, 其实是典型的线程不安全代码. 

比如, 我们现在有两个线程同时调用getInstance方法. 线程t1先执行if判断, 判断出instance为空. 此时t2线程插入进来了, 执行t2线程的if判断, 那么t2的判断结果同样是空, t2就会执行new SingletonLazy() 创建出一个新的对象. 而再切换回t1线程, 由于t1对instance的判断也为空, 所以, t1也会执行new SingletonLazy() 创建出一个新的对象. 那么这样的话, Singletonlazy类就被实例化了两次. 而单例模式要求类只能被实例化一次.

([注]: 虽然说后面创建的实例覆盖了前面创建的实例, 前面创建的实例没有引用变量引用的话很块回被销毁回收, 但是创建实例对象这个过程本身的开销就很大(比如有的类一个实例就要100个G), 所以我们仍然认为这个代码是有bug的)

所以, 为了解决上述线程不安全问题, 我们就需要进行"加锁"操作. 将条件判断和创建操作作为一个整体加上锁, 这样一来, if判断和new创建操作这个整体就成了一个"原子"操作.  这就保证了某个线程在顺序执行这两个操作的时候不会有别的线程插入进来.

class SingletonLazy {
    private static SingletonLazy instance = null;
    public static Object locker = new Object(); 
    public static SingletonLazy getInstance() {
        synchronized(locker) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
            return instance;
            // 如果instance为空,则创建一个实例对象; 如果instance不为空,则直接返回instance
        }
    }
    private SingletonLazy() {}
}

试想一下, 上述代码, 如果已经完成了创建对象的操作之后, 后续如果再调用getInstance, 就再也不会进入if分支中去了, 都是简单的读操作(return instance). 那么只有读操作的话, 不加锁也是线程安全的. 我们知道, 加锁这个操作, 对程序性能的影响还是挺大的.  所以, 我们只需要在第一次执行这个方法的时候(没有创建出对象的时候)加锁即可, 其他时候再执行这个方法, 都是线程安全的, 不需要加锁. 

那么, 如何判断当前是不是第一次调用这个方法呢? --> 看是否已经创建出了实例对象, 如果还没有instance对象, 那就是第一次调用, 需要对里面的判断-创建对象 操作 加锁;  如果已经有了instance对象, 那就不是第一次调用, 就不需要加锁, 直接返回instance.

依据上述思考, 我们对代码做出如下修改:

 public static SingletonLazy getInstance() {
        if (instance == null) { //外层if: 判断是否应该加锁
            synchronized(locker) {
                if (instance == null) { //内层if: 判断是否要创建对象
                    instance = new SingletonLazy();
                }
                // 如果instance为空,则创建一个实例对象; 如果instance不为空,则直接返回instance
            }
        }
        return instance;
    }

注意: 这里, 外层if和内层if虽然条件恰好是一样的, 但是作用是完全不同的. 外层的if作用是: 判断是否要加锁. 内层if的作用是: 判断是否要创建新的对象.

  • 指令重排序问题

编译器在执行创建对象的代码时, 为了提高性能, 可能会进行"指令重排序"操作.

 instance = new SingletonLazy();

编译器在执行这个创建对象代码的时候, 会经过如下步骤: (1) 分配内存空间.  (2) 执行构造方法.  (3) 将对象的内存空间地址赋给引用变量.   正常来说, 是按照(1) -> (2) -> (3) 的顺序执行的. 但是编译器为了优化性能, 也可能按照(1) -> (3) -> (2) 的顺序执行.

 public static SingletonLazy getInstance() {
        if (instance == null) { //外层if: 判断是否应该加锁
            synchronized(locker) {
                if (instance == null) { //内层if: 判断是否要创建对象
                    instance = new SingletonLazy();
                }
                // 如果instance为空,则创建一个实例对象; 如果instance不为空,则直接返回instance
            }
        }
        return instance;
    }

那么, 我们试想, 如果在执行完(1) -> (3) 之后, 此时有别的线程切入, 执行if (instance == null) 判定, 那么此时判定instance就不为空了, 因为语句指向了内存空间(即使这个内存空间里什么都没有). 判定完instance不为空之后, 就会直接return instance. 那么如果这个线程拿到instance之后, 如果再调用里面的某个方法. 那么此时就会出现错误!!! (因为instance指向的内存空间是未初始化的).

那么如何解决这个情况呢? --> volatile. 我们可以在instance前面加上一个volatile修饰, 告诉系统, instance这个引用是"易变的, 易失的". 那么此时系统就会放弃对new SingletonLazy() 这个创建对象操作的优化, 按照(1) -> (2) -> (3) 的顺序执行创建对象操作.这样的话, 就不会出现上述问题了~

加上volatile的代码最终如下:

class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    public static Object locker = new Object();
    public static SingletonLazy getInstance() {
        if (instance == null) { //外层if: 判断是否应该加锁
            synchronized(locker) {
                if (instance == null) { //内层if: 判断是否要创建对象
                    instance = new SingletonLazy();
                }
                // 如果instance为空,则创建一个实例对象; 如果instance不为空,则直接返回instance
            }
        }
        return instance;
    }
    private SingletonLazy() {}
}

那么这样一个单例模式的代码无论在执行效率还是在线程安全上就都没有任何问题了.

好了, 本篇文章就介绍到这里啦, 大家如果有疑问欢迎评论, 如果喜欢小编的文章, 记得点赞收藏~~

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

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

相关文章

STM32软件开发 —— STM32CudeMX使用优点

目 录 STM32CudeMX使用思路步骤详细 STM32CudeMX 在图形化工具STM32CudeMX出现之前,开发者通常是参考库驱动文件中的例程来配置芯片的,进行拷贝和修改等,为了提高开发效率,ST公司开发了STM32CudeMX工具,通过它简化了芯…

江西省补贴性线上职业技能培训管理平台(刷课系统)

江西省补贴性线上职业技能培训管理平台(刷课系统) 目的是为了刷这个网课 此系统有两个版本一个是脚本运行,另外一个是可视化界面运行 可视化运行 技术栈:flask、vue3 原理: 通过分析网站接口,对某些接口加密的参数进行逆向破解,从而修改请求…

Golang | Leetcode Golang题解之第546题移除盒子

题目: 题解: func removeBoxes(boxes []int) int {dp : [100][100][100]int{}var calculatePoints func(boxes []int, l, r, k int) intcalculatePoints func(boxes []int, l, r, k int) int {if l > r {return 0}if dp[l][r][k] 0 {r1, k1 : r, k…

es自动补全(仅供自己参考)

elasticssearch提供了CompletionSuggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询效率,对于文档中字段的类型有一些约束: 查询类型必须是:completion 字段内容是多个补全词条形成的数组 P…

ANDROIDWORLD: A Dynamic Benchmarking Environment for Autonomous Agents论文学习

这个任务是基于androidenv的。这个环境之前学过,是一个用来进行强化学习的线上环境。而这篇文章的工作就是要给一些任务加上中间的奖励信号。这种训练环境的优点就是动态,与静态的数据集(比如说我自己的工作)不同,因此…

VBA10-处理Excel的动态数据区域

一、end获取数据边界 1、基本语法 1-1、示例: 2、配合row和column使用 2-1、示例1 2-2、示例2 此时,不管这个有数值的区域,怎么增加边界,对应的统计数据也会跟着变的! 二、end的缺陷 若是数据区域不连贯,则…

【FFmpeg】FFmpeg 函数简介 ③ ( 编解码相关函数 | FFmpeg 源码地址 | FFmpeg 解码器相关 结构体 和 函数 )

文章目录 一、FFmpeg 解码器简介1、解码流程分析2、FFmpeg 编解码器 本质3、FFmpeg 编解码器 ID 和 名称 二、FFmpeg 解码器相关 结构体 / 函数1、AVFormatContext 结构体2、avcodec_find_decoder 函数 - 根据 ID 查找 解码器3、avcodec_find_decoder_by_name 函数 - 根据 名称…

Linux完结

学习视频笔记均来自B站UP主" 泷羽sec",如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 【linux基础之病毒编写(完结)】 https://www.bilibili.com/video…

分享三个python爬虫案例

一、爬取豆瓣电影排行榜Top250存储到Excel文件 近年来,Python在数据爬取和处理方面的应用越来越广泛。本文将介绍一个基于Python的爬虫程序,用于抓取豆瓣电影Top250的相关信息,并将其保存为Excel文件。 获取网页数据的函数,包括以…

【计网】数据链路层笔记

【计网】数据链路层 数据链路层概述 数据链路层在网络体系结构中所处的地位 链路、数据链路和帧 链路(Link)是指从一个节点到相邻节点的一段物理线路(有线或无线),而中间没有任何其他的交换节点。 数据链路(Data Link)是基于链路的。当在一条链路上传送数据时&a…

docker 拉取MySQL8.0镜像以及安装

目录 一、docker安装MySQL镜像 搜索images 拉取MySQL镜像 二、数据挂载 在/root/mysql/conf中创建 *.cnf 文件 创建容器,将数据,日志,配置文件映射到本机 检查MySQL是否启动成功: 三、DBeaver数据库连接 问题一、Public Key Retrieval is not allowed 问题…

【c++篇】:栈、队列、优先队列:容器世界里的秩序魔法 - stack,queue与priority_queue探秘

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨ ✨ 个人主页:余辉zmh–CSDN博客 ✨ 文章所属专栏:c篇–CSDN博客 文章目录 前言一.容器stack1.介绍2.成员函数3.模拟实现4.注意事项 二.容器qu…

实现uniapp-微信小程序 搜索框+上拉加载+下拉刷新

pages.json 中的配置 { "path": "pages/message", "style": { "navigationBarTitleText": "消息", "enablePullDownRefresh": true, "onReachBottomDistance": 50 } }, <template><view class…

无人机培训机型有哪些?CAAC考证选3类还是4类

无人机培训是一个涵盖多个方面的综合性过程&#xff0c;旨在培养具备无人机操作技能和相关知识的人才。 无人机培训机型 无人机培训通常涵盖多种机型&#xff0c;以满足不同领域和应用场景的需求。常见的无人机培训机型包括&#xff1a; 1. 多旋翼无人机&#xff1a;也称为多…

95.【C语言】数据结构之双向链表的头插,头删,查找,中间插入,中间删除和销毁函数

目录 1.双向链表的头插 方法一 方法二 2.双向链表的头删 3.双向链表的销毁 4.双向链表的某个节点的数据查找 5.双向链表的中间插入 5.双向链表的中间删除 6.对比顺序表和链表 承接94.【C语言】数据结构之双向链表的初始化,尾插,打印和尾删文章 1.双向链表的头插 方法…

[极客大挑战 2019]PHP 1

[极客大挑战 2019]PHP 1 审题 猜测备份在www.zip中&#xff0c;输入下载文件。 知识点 反序列化 解题 查看代码 看到index.php中包含了class.php,直接看class.php中的代码 查看条件 当usernameadmin&#xff0c;password100时输出flag 构造反序列化 输入select中&#…

C++面试基础知识:排序算法 C++实现

上周实习面试&#xff0c;手撕代码快排没写出来&#xff0c;非常丢人&#xff0c;把面试官都给逗笑了。 基础不牢&#xff0c;地动山摇&#xff0c;基础的算法还是要牢记于心的。 插入排序 分为有序区和无序区&#xff0c;每次从无序区中选出一个&#xff0c;放到有序区域中。…

yarn报错`warning ..\..\package.json: No license field`:已解决

出现这个报错有两个原因 1、项目中没有配置许可证 在项目根目录package.json添加 {"name": "next-starter","version": "1.0.0",# 添加这一行"license": "MIT", }或者配置私有防止发布到外部仓库 {"priv…

批量缓存模版

批量缓存模版 缓存通常有两种使用方式&#xff0c;一种是Cache-Aside&#xff0c;一种是cache-through。也就是旁路缓存和缓存即数据源。 一般一种用于读&#xff0c;另一种用于读写。参考后台服务架构高性能设计之道。 最典型的Cache-Aside的样例&#xff1a; //读操作 da…

亚信安全并购亚信科技交易正式完成

亚信安全与亚信科技联合宣布&#xff0c;亚信安全正式完成对亚信科技的控股权收购&#xff0c;由此&#xff0c;规模近百亿的中国最大的软件企业之一诞生&#xff01;双方将全面实现公司发展战略&#xff0c;以及优势能力与资源的深度融合&#xff0c;形成业界独有的“懂网、懂…