单例模式有几种写法?请谈谈你的理解?

news2024/11/17 21:53:49

为什么有单例模式?

单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。


实现原理是什么?

构造方法是private+static方法+if语句判断
注意:不同的实现方式它的实现原理肯定是有所区别的,综合来看!!


实现方式有哪些?

懒汉式、双重锁、饿汉式、静态内部类、枚举


懒汉式

  • 好处:启动速度快、节省资源,一直到实例被第一次访问,才需要初始化单例、避免空间浪费;
  • 缺点:线程不安全,if语句存在竞态条件

单例类

package com.example;

/**
 * @BelongsProject: BigK
 * @BelongsPackage: com.example
 * @Author: dengLiMei
 * @CreateTime: 2023-06-28  10:04
 * @Description: 单例模式
 * @Version: 1.0
 */
public class Singleton {
    //提供一个全局变量让全局访问
    private static Singleton instance;

    //私有构造方法,堵死外界利用new创建此类实例的可能
    private Singleton() {
    }

    //获得实例的唯一全局访问点
    public static Singleton GetInstance() {
        //当多线程来临的时候判断是否为null,此时instance就是临界资源,会实例化多个
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

客户端

//反射破坏封装性
Singleton instance1 = Singleton.GetInstance();

// 使用反射获取私有构造函数
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);

// 通过反射创建第二个实例
Singleton instance2 = constructor.newInstance();

System.out.println(instance1); // 输出第一个实例的内存地址
System.out.println(instance2); // 输出第二个实例的内存地址

这里我是通过反射的方式去获取对象,然后对获取到的对象进行判断,运行代码之后我们会发现:
在这里插入图片描述
两个对象的内存地址并不相同,违背了单一性,那我们如何解决这个问题呢?可能屏幕前有些小伙伴想到了加锁的方式去做,没错,我们用大家比较常见的synchronized实现看看吧。


懒汉式变种-synchronized

  • 好处:线程安全
  • 缺点:并发性能差,synchronized加锁,不管有没有对象都加锁
    单例类
package com.example;

/**
 * @BelongsProject: BigK
 * @BelongsPackage: com.example
 * @Author: dengLiMei
 * @CreateTime: 2023-06-28  10:14
 * @Description: 懒汉单例:在第一次被引用时,才会将自己实例化
 * @Version: 1.0
 */
public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {
        System.out.println("创建一次");
    }


    public static LazySingleton GetInstance() {
        //方法一:加锁-把判断的这部分逻辑上锁
        //好处:线程安全
        //缺点:并发性能差,synchronized加锁,不管有没有对象都加锁
        //解决方案:双重锁
        synchronized ("") {
            if (instance == null) {
                instance = new LazySingleton();
            }
        }
        return instance;
    }

    //方法二:同步代码段
    public static synchronized LazySingleton getSingleton() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

客户端

  //懒汉模式:加锁保证线程安全
Runnable r3 = () -> {

DoubleLockSingleton s1 = DoubleLockSingleton.GetInstance();
DoubleLockSingleton s2 = DoubleLockSingleton.GetInstance();

if (s1 == s2) {
	System.out.println("两个对象是相同的实例");
}
};

Thread t1 = new Thread(r3);
Thread t2 = new Thread(r3);

t1.start();
t2.start();

在这里插入图片描述
通过运行结果我们会发现两个线程获取到的对象是同一个,实现了单例。
但是大家可以思考一下这样会不会存在什么问题呢?线程因为每次访问 getInstance() 方法时都需要获取锁,即使实例已经被创建,会在高并发环境下其实是比较影响性能的。并且会导致每次调用 getInstance() 方法都需要获取锁,而不是在需要时才创建实例。那我们可不可以当单例对象没有被创建的时候才去加锁呢?双重锁可以做到

懒汉式变种-双重锁

  • 好处:实现线程安全地创建实例,而又不会对性能造成太大影响。
  • 缺点:无效等待,同步效率地,锁占用资源(反射会破坏单一性)
    单例类
package com.example;

/**
 * @BelongsProject: BigK
 * @BelongsPackage: com.example
 * @Description: 懒汉单例——双重锁
 * @Version: 1.0
 */
public class DoubleLockSingleton {
    //volatile:禁止指令重排序(防止部分初始化)
    private static volatile DoubleLockSingleton instance;

    private DoubleLockSingleton() {

        System.out.println("实例化了一次");
    }

    //原理:双重if,延迟实例化,避免每次进行同步的性能开销
    public static DoubleLockSingleton GetInstance() {
        //第一层判断:先判断实例是否存在,不存在再加锁处理
        if (instance == null) {
            synchronized ("") {
                //第二层判断
                if (instance == null) {
                    instance = new DoubleLockSingleton();
                }
            }
        }
        return instance;
    }
}

客户端

DoubleLockSingleton instance1 = DoubleLockSingleton.GetInstance();

// 使用反射获取私有构造函数
Constructor<DoubleLockSingleton> constructor = DoubleLockSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);

// 通过反射创建第二个实例
DoubleLockSingleton instance2 = constructor.newInstance();

System.out.println(instance1); // 输出第一个实例的内存地址
System.out.println(instance2); // 输出第二个实例的内存地址

这里我们依旧使用反射去获取单例对象。我们运行看看效果:
在这里插入图片描述
发现构造方法被调用了两地,并且获取到的两个对象的地址也不同,依旧是破坏了单例性。
双重锁实现方式是在第一次创建实例的时候同步,以后就不需要同步了。反射的使用让我们的单例类又不攻自破,没关系,咱们还有其他方式——饿汉式


饿汉式

  • 优点:类加载阶段创建,保证了线程安全
  • 缺点:可能存在没有被使用的可能,造成资源浪费

单例类

package com.example;

/**
 * 饿汉模式:类加载时初始化单例,以后访问时直接返回即可
 */
public class HungrySingleton {
    //类加载阶段就实例化
    private static final HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return singleton;
    }
}

客户端

//获取单例对象
HungrySingleton singleton = HungrySingleton.getInstance();

// 使用反射获取单例对象
try {
	Class<?> singletonClass = Class.forName("com.example.HungrySingleton");

	// 获取私有构造函数
	Constructor<?> constructor = singletonClass.getDeclaredConstructor();
	constructor.setAccessible(true);

	// 通过反射实例化对象
	HungrySingleton singletonReflection = (HungrySingleton) constructor.newInstance();

	// 验证是否为同一对象
	System.out.println(singleton == singletonReflection);  // 输出 true
} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
	e.printStackTrace();
}

使用反射获取单例对象,我们看下输出结果:
在这里插入图片描述
在整个应用程序的生命周期中,无论是否会用到该单例实例,都会在类加载时创建实例,可能会导致资源的浪费。饿汉模式无法实现延迟加载,即在需要时才创建实例。这可能会导致在应用程序启动时就创建了大量的实例,占用内存。
基于这些原因,尽管饿汉模式是一种简单且线程安全的单例模式实现方式,但在资源利用、延迟加载和异常处理等方面存在一些问题。所以我们在实际使用过程中需要根据具体场景选择合适的单例模式实现方式


静态内部类

好处:

  • 懒加载:静态内部类的方式能够实现懒加载,即在需要时才会加载内部类,从而创建单例对象。这样可以避免在类加载时就创建单例对象,节省了资源。
  • 线程安全:静态内部类的方式利用了类加载机制和静态变量的特性,能够保证在多线程环境下也能够保持单例的唯一性,而且不需要使用同步关键字。
  • 延迟加载:由于静态内部类的加载是在需要时才进行的,因此能够实现延迟加载,即在第一次使用时才会创建单例对象。

缺点:静态内部类的方式需要额外的类加载和内存开销,因为它需要创建一个内部类对象,而内部类对象的创建需要额外的内存开销。

单例类

package com.example;

/**
 * 静态内部类
 * 
 */
public class StaticInnerSingleton {
    //静态内部类
    private static class SingletonHolder {
        private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }

    private StaticInnerSingleton (){}


    public static final StaticInnerSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

}

客户端

//获取单例对象
StaticInnerSingleton singleton = StaticInnerSingleton.getInstance();

// 使用反射获取单例对象
try {
	Class<?> singletonClass = Class.forName("com.example.StaticInnerSingleton");
	
	// 获取私有构造函数
	Constructor<?> constructor = singletonClass.getDeclaredConstructor();
	constructor.setAccessible(true);
	
	// 通过反射实例化对象
	StaticInnerSingleton singletonReflection = (StaticInnerSingleton) constructor.newInstance();
	
	// 验证是否为同一对象
	System.out.println(singleton == singletonReflection);  // 输出 true
} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
	e.printStackTrace();
}

我们来看看运行结果:
在这里插入图片描述
获取的两个对象的地址是不相同的,实现了单例。
它利用了类加载的特性和静态内部类的懒加载特性,解决了饿汉模式的资源浪费和懒汉模式的线程安全问题。具体实现方式是在外部类中定义一个私有的静态内部类,内部类中创建单例实例,并且利用类加载的特性保证了实例的唯一性。同时,由于静态内部类是在需要的时候才加载,因此实现了延迟加载的效果。也是比较推荐的一种方式


枚举

优点:线程安全、防止反序列化重新创建新的对象
单例类

package com.example;

/**
 * 枚举方式
 */
public enum EnumSingleton {
    INSTANCE;
}

客户端

 // 获取单例对象
EnumSingleton singleton1 = EnumSingleton.INSTANCE;
EnumSingleton singleton2 = EnumSingleton.INSTANCE;

// 验证是否为同一对象
System.out.println(singleton1 == singleton2);  // 输出 true

我们来让控制台打印输出看看结果:
在这里插入图片描述
在Java中,枚举类型是线程安全的,并且保证在任何情况下都是单例的。因此,使用枚举实现单例模式是一种推荐的方式。具体实现方式是定义一个包含单个枚举常量的枚举类型,这个枚举常量就是单例实例。由于枚举类型在Java中是天然的单例,因此不需要担心线程安全和反射攻击等问题。


使用场景有哪些?

Windows的Task Manager(任务管理器)、回收站


使用时如何选择?

在这里插入图片描述
在实际业务场景中,可以根据具体需求选择适合的单例模式。如果需要在应用启动时创建对象,且对性能要求较高,可以选择饿汉式或双重校验锁;如果需要延迟加载对象,可以选择静态内部类或枚举单例模式;如果对线程安全要求较高,可以选择双重校验锁或静态内部类单例模式



如果有想要交流的内容欢迎在评论区进行留言,如果这篇文档受到了您的喜欢那就留下你点赞+收藏+评论脚印支持一下博主~

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

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

相关文章

【C语言】探索数据结构:单链表和双链表

目录 &#x1f4a1;链表的概念和结构 &#x1f4a1;链表的分类 &#x1f4a1;无头单向非循环链表&#xff08;单链表&#xff09;的实现 定义节点结构 单链表的尾部插入 单链表的头部插入 单链表的尾部删除 单链表的头部删除 在指定位置插入前数据 在指定位置之后插入数…

如何发布一款移动 App?

如何发布一款移动 App&#xff1f; 本文转自 公众号 ByteByteGo&#xff0c;如有侵权&#xff0c;请联系&#xff0c;立即删除 今天来聊聊如何发布一款移动 App。 移动 App 的发布流程不同于传统方法。下图简化了这一过程&#xff0c;以帮助您理解。 移动应用程序发布流程的典…

基于simulink的模糊PID控制器建模与仿真,并对比PID控制器

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1PID控制器原理 4.2 模糊PID控制器原理 5.完整工程文件 1.课题概述 在simulink&#xff0c;分别建模实现一个模糊PID控制器和一个PID控制器&#xff0c;然后将PID控制器的控制输出和模糊PID的控制输出…

TCP四次握手

TCP 协议在关闭连接时&#xff0c;需要进行四次挥手的过程&#xff0c;主要是为了确保客户端和服务器都能正确地关闭连接。 # 执行流程 四次挥手的具体流程如下&#xff1a; 客户端发送 FIN 包&#xff1a;客户端发送一个 FIN 包&#xff0c;其中 FIN 标识位为 1&#xff0c…

x2openEuler 升级实操(centos7.8 to openEuler 20.03)

通过 x2openEuler 工具&#xff0c;将 centos 7.8 迁移至 OpenEuler 上&#xff0c;实际感受迁移过程。x2openEuler https://docs.openeuler.org/zh/docs/20.03_LTS_SP1/docs/x2openEuler/x2openEuler.html 环境准备 下载 x2openEuler 安装包 wget https://repo.oepkgs.net/o…

Spring AOP原理的常见面试题

Spring AOP原理的常见面试题 .Spring AOP是怎么实现的什么是代理模式静态代理动态代理怎么实现的JDK动态代理CGLIB动态代理引入依赖 JDK与CJLIB的区别什么时候使用JDK与CJLIB . Spring AOP是怎么实现的 答:Spring AOP是通过动态代理来实现AOP的 什么是代理模式 答:代理模式也…

C++数据结构与算法——链表

C第二阶段——数据结构和算法&#xff0c;之前学过一点点数据结构&#xff0c;当时是基于Python来学习的&#xff0c;现在基于C查漏补缺&#xff0c;尤其是树的部分。这一部分计划一个月&#xff0c;主要利用代码随想录来学习&#xff0c;刷题使用力扣网站&#xff0c;不定时更…

LeetCode 834. 树中距离之和

简单换根DP 其实就是看好变化量&#xff0c;然后让父亲更新儿子就好了&#xff5e; 上图2当根节点的时候&#xff0c;ans[2] ans[0] -sz[2]n-sz[2]; class Solution { public:vector<int> sumOfDistancesInTree(int n, vector<vector<int>>& edges) {v…

OCP NVME SSD规范解读-8.SMART日志要求-2

SMART-7&#xff1a; 软错误ECC计数可能是记录了被第一级ECC&#xff08;比如LDPC Hard Decode&#xff09;成功纠正过的读取错误次数。这意味着数据恢复成功&#xff0c;但依然表明存储介质出现了某种程度上的可靠性下降。 LDPC码是一种基于稀疏矩阵的纠错码&#xff0c;它由…

鸿蒙会取代Android吗?听风就是雨

现在说取代还谈不上&#xff0c;毕竟这需要时间。安卓作为全球第一的手机操作系统&#xff0c;短时间内还无法取代。持平iOS甚至超过iOS有很大可能&#xff0c;最终会呈现“三足鼎立”有望超过安卓基数。 作为全新的鸿蒙操作系统&#xff0c;其现在已经是全栈自研底座。按照鸿…

【前端工程化】环境搭建 nodejs npm

文章目录 前端工程化是什么&#xff1f;前端工程化实现技术栈前端工程化环境搭建 &#xff1a;什么是Nodejs如何安装nodejsnpm 配置和使用npm 介绍npm 安装和配置npm 常用命令 总结 前端工程化是什么&#xff1f; 前端工程化是使用软件工程的方法来单独解决前端的开发流程中模块…

模拟电路之运放

滞回比较器&#xff1a; 小幅度波动时候不受影响&#xff0c;除非超过一点范围 当输入信号慢慢增加到UT&#xff0c;就变成负电压 当输入信号慢慢减压到—UT&#xff0c;就变成正电压 电路反向接信号 正反馈&#xff0c;串联电阻接地 调整回差的方法 1.调整电阻的分压 2.…

python实现贪吃蛇小游戏(附源码)

文章目录 导入所需的模块坐标主游戏循环模块得分 贪吃蛇小游戏&#xff0c;那个曾经陪伴着00后和90后度过无数欢笑时光的熟悉身影&#xff0c;仿佛是一把打开时光之门的钥匙。它不仅是游戏世界的经典之一&#xff0c;更是我们童年岁月中不可或缺的一部分&#xff0c;一个承载回…

使用宝塔面板访问MySQL数据库

文章目录 前言一、安装访问工具二、查看数据库总结 前言 前面我们已经部署了前后端项目&#xff0c;但是却不能得到数据库的信息&#xff0c;看有谁再使用你的项目。例如员工、用户等等。本次博客进行讲解如何在宝塔面板里面访问MySQL数据库。 一、安装访问工具 1、打开软件商…

微信小程序(二十六)列表渲染基础核心

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.列表渲染基础写法 2.外部索引和自身索引 源码&#xff1a; index.wxml <view class"students"><view class"item"><text>序号</text><text>姓名</text…

C++-内存管理(1)

1. C/C内存分布 首先我们需要知道&#xff0c;在C中的内存分为5个区。 1. 栈 又叫堆栈 -- 非静态局部变量 / 函数参数 / 返回值等等&#xff0c;栈是向下增长的。 2. 内存映射段 是高效的 I/O 映射方式&#xff0c;用于装载一个共享的动态内存库。用户可使用系统接口 创建…

微调入门篇:大模型微调的理论学习

1、为什么大模型微调 之前在《大模型这块蛋糕,想吃吗》介绍了普通人如何搭上大模型这块列车, 其中有一个就是模型微调,这个也是未来很多IT公司需要发力的方向,以及在《垂直领域大模型的应用更亲民》中论述了为什么微调适合大家,以及微调有什么价值? 作为小程序猿在开始进行微…

C#,打印漂亮的贝尔三角形(Bell Triangle)的源程序

以贝尔数为基础&#xff0c;参考杨辉三角形&#xff0c;也可以生成贝尔三角形&#xff08;Bell triangle&#xff09;&#xff0c;也称为艾特肯阵列&#xff08;Aitkens Array&#xff09;&#xff0c;皮埃斯三角形&#xff08;Peirce Triangle&#xff09;。 贝尔三角形的构造…

常用抓包软件集合(Fiddler、Charles)

1. Fiddler 介绍&#xff1a;Fiddler是一个免费的HTTP和HTTPS调试工具&#xff0c;支持Windows平台。它可以捕获HTTP和HTTPS流量&#xff0c;并提供了丰富的调试和分析功能。优点&#xff1a;易于安装、易于使用、支持多种扩展、可以提高开发效率。缺点&#xff1a;只支持Wind…

Linux内核源码

记得看目录哦&#xff01; 1. 为什么要阅读Linux内核2. Linux0.01内核源码3. 阅读linux内核源码技巧4. linux升级内核5. linux的备份和恢复5.1 安装dump和restore5.2 使用dump完成备份5.3 使用restore完成恢复 1. 为什么要阅读Linux内核 2. Linux0.01内核源码 3. 阅读linux内核…