[单例模式]

[设计模式]

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

不同语言适用的设计模式是不一样的. 这里我们接下来要谈到的是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() {}
}

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MethodChannel插件的用法

文章目录 1 知识回顾2 示例代码3 经验总结我们在上一章回中介绍了通道相关的内容,本章回中将介绍其中的一种通道:MethodChannnel.闲话休提,让我们一起Talk Flutter吧。 1 知识回顾 我们在上一章回中介绍了通道的概念和作用,并且提到了通道有不同的类型,本章回将其中一种通…

Golang | Leetcode Golang题解之第554题砖墙

题目: 题解: func leastBricks(wall [][]int) int {cnt : map[int]int{}for _, widths : range wall {sum : 0for _, width : range widths[:len(widths)-1] {sum widthcnt[sum]}}maxCnt : 0for _, c : range cnt {if c > maxCnt {maxCnt c}}retur…

通讯录(C 语言)

目录 一、通讯录设计思路1. 伪代码设计思路2. 代码设计思路 二、代码实现三、程序运行演示四、整体分析 一、通讯录设计思路 1. 伪代码设计思路 通讯录可以用来存储 100 个人的信息,每个人的信息包括:姓名、性别、年龄、电话、住址。 提供方法&#x…

深入解析四种核心网络设备:集线器、桥接器、路由器和交换机

计算机网络系列课程《网络核心设备》 在现代网络技术中,集线器、桥接器、路由器和交换机扮演着至关重要的角色。本文,将深入探讨这四种设备的功能、工作原理及其在网络架构中的重要性。 集线器:基础网络连接设备 集线器(Hub&…

01 Oracle 数据库存储结构深度解析:从数据文件到性能优化的全链路探究

文章目录 Oracle 数据库存储结构深度解析:从数据文件到性能优化的全链路探究一、Oracle存储结构的物理层次1.1 控制文件(Control File)1.2 联机重做日志文件(Online Redo Log File)1.3 数据文件(Data File&…

Type-C转DP线方案

在现代数字化生活中,高清视频传输已成为日常需求的重要组成部分。无论是工作中的多屏协作,还是娱乐中的沉浸式体验,高清显示器都扮演着不可或缺的角色。然而,随着设备接口的多样化,如何高效地将Type-C设备连接至Displa…

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

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

Java基础——循环switch大数值更改器访问器深浅拷贝

目录 1.循环 2.switch多分支选择结构 3.大数值 4.浅拷贝&深拷贝 5.Arrays.sort排序 6.面向对象 7.更改器&访问器 1.循环 for-each循环 在 Java 中,for-each 循环(也称为增强型 for 循环)是一种用于遍历数组或集合&#xff08…

引入 axios,根据 api 文档生成调用接口

起步 | Axios Docs 安装 axios npm install axios 生成 api 调用接口【可选】 https://github.com/ferdikoomen/openapi-typescript-codegen 安装 npm install openapi-typescript-codegen --save-dev 然后执行生成代码 # http://localhost:8805/api/user/v3/api-docs&a…

wsl2+Ubuntu安装图形化界面(VNC方式)

本章教程,记录在wsl2+Ubuntu操作系统中安装可视化界面步骤。 一、安装桌面环境 尽量以root用户进行安装,避免因权限不足导致部分依赖无法安装。 sudo apt update sudo apt upgrade -y sudo apt install ubuntu-desktop由于可视化桌面程序安装包比较大,网速慢的小伙伴,需要耐…

Linux应用——线程池

1. 线程池要求 我们创建线程池的目的本质上是用空间换取时间,而我们选择于 C 的类内包装原生线程库的形式来创建,其具体实行逻辑如图 可以看到,整个线程池其实就是一个大型的 CP 模型,接下来我们来完成它 2. 整体模板 #pragma …

学习日记_241110_局部线性嵌入(Locally Linear Embedding, LLE)

前言 提醒: 文章内容为方便作者自己后日复习与查阅而进行的书写与发布,其中引用内容都会使用链接表明出处(如有侵权问题,请及时联系)。 其中内容多为一次书写,缺少检查与订正,如有问题或其他拓展…

【我的 Anti-AV 学习手札】DLL注入

无敌舍友s神免杀学了一个阶段,达者为师,向s师傅学习!! ps:我的基础实在薄弱,WIN编程甚至都没做过,所以笔记翔实些 一、注入思路 1.在进程中开辟一段空间 2.存入dll绝对路径地址的字符串 3.使用…

【HarmonyOS NEXT】一次开发多端部署(以轮播图、Tab栏、列表为例,配合栅格布局与媒体查询,进行 UI 的一多开发)

关键词:一多、响应式、媒体查询、栅格布局、断点、UI 随着设备形态的逐渐增多,应用界面适配也面临着很大问题,在以往的安卓应用开发过程中,往往需要重新开发一套适用于大屏展示的应用,耗时又耗力,而鸿蒙提供…

linux 安装 mongodb

选择MongoDB版本 https://www.mongodb.com/try/download/community-kubernetes-operator 我的系统是centos7.9 这里只能最高只能选择mongo7 复制下载链接:https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-7.0.15.tgz 获取安装教程: h…

《深入浅出Apache Spark》系列②:Spark SQL原理精髓全解析

导读:SQL 诞生于 20 世纪 70 年代,至今已有半个世纪。SQL 语言具有语法简单,低学习门槛等特点,诞生之后迅速普及与流行开来。由于 SQL 具有易学易用的特点,使得开发人员容易掌握,企业若能在其计算机软件中支…

PostgreSQL pg-xact(clog)目录文件缺失处理

一、 背景 前些天晚上突然收到业务反馈,查询DB中的一个表报错 Could not open file "pg-xact/005E": No such file or directory. 两眼一黑难道是文件损坏了...登录查看DB日志,还好没有其他报错,业务也反馈只有这一个表在从库查询报…

Cursor的chat与composer的使用体验分享

经过一段时间的试用,下面对 Composer 与 Chat 的使用差别进行总结: 一、长文本及程序文件处理方面 Composer 在处理长文本时表现较为稳定,可以对长文进行更改而不会出现内容丢失的情况。而 Chat 在更改长的程序文件时,有时会删除…

MATLAB课程:AI工具辅助编程——MATLAB+LLMs

给出一些可能有用的方法辅助大家写代码。 方法一:MATLAB软件LLM (不太懂配置的同学们为了省事可以主要用这个方法) 方法一特别针对本门MATLAB教学课程,给出一种辅助ai工具的操作指南。MATLAB中可以安装MatGPT插件,该插件通过调用ChatGPT的API…

2.索引:SQL 性能分析详解

SQL性能分析是数据库优化中重要的一环。通过分析SQL的执行频率、慢查询日志、PROFILE工具以及EXPLAIN命令,能够帮助我们识别出数据库性能的瓶颈,并做出有效的优化措施。以下将详细讲解这几种常见的SQL性能分析工具和方法。 一、SQL 执行频率 SQL执行频率…