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

为什么有单例模式?

单例模式(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.hqwc.cn/news/443165.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mysql聚合函数

聚合函数又称分组函数 多行(一组)数据 返回一个结果 数据表 链接&#xff1a;https://pan.baidu.com/s/1dPitBSxLznogqsbfwmih2Q 提取码&#xff1a;b0rp --来自百度网盘超级会员V5的分享 聚合函数会自动排除掉null字段 AVG SUM 适用于数值类型 MIN MAX 适用于数值…

Python中类的相关术语(附带案例)

目录 1、面向对象 2、类 3、实例 4、初始化方法 5、魔法方法 6、字符串方法 7、self 8、数据、属性、操作、行为 9、父类、基类、超类 or 子类、派生类 10、多态 11、重载多态 and 重写多态 12、名称解释 1、面向对象 在Python中&#xff0c;面向对象编程&…

实现单点登录

指再多系统应用群中登录一个系统&#xff0c;便可在其他所有系统中得到授权而无需再次登录&#xff0c;包括单点登录与单点注销两部分。 相比于单系统登录&#xff0c;sso需要一个独立的认证中心&#xff0c;只有认证红心能接受用户的用户名密码等安全信息&#xff0c;其他系统…

nodejs详细介绍2以及vue脚手架

前言 首先给宝子们说声抱歉&#xff0c;因为自己在使用vscode执行vue脚手架的时候出现了很多问题&#xff0c;在经过解决之后还是有一个对应的我弄了一天没弄好&#xff0c;就是“vscode的终端将被任务重用&#xff0c;按任意键关闭”然后导致的是自己没心情学习一点&#xff…

人工视觉仍然需要图像采集卡

最初&#xff0c;图像采集卡被用作模拟视频数字转换器和图像缓冲器&#xff0c;但如今它们能够执行复杂的任务&#xff0c;例如图像处理。图像采集卡的设计不断发展&#xff0c;旨在提高系统性能并减少计算机处理需求。 除了图像采集之外&#xff0c;图像采集卡还执行机器视觉…

WebService的services.xml问题

WebService有多种实现方式&#xff0c;这里使用的是axis2 问题&#xff1a; 在本地开发&#xff0c;访问本地的http://localhost:8080/services/ims?wsdl&#xff0c;正常访问 但是打成jar包&#xff0c;不管是linux还是window启动&#xff0c;都访问不到&#xff0c;报错…

构建基于Flask的跑腿外卖小程序

跑腿外卖小程序作为现代生活中的重要组成部分&#xff0c;其技术实现涉及诸多方面&#xff0c;其中Web开发框架是至关重要的一环。在这篇文章中&#xff0c;我们将使用Python的Flask框架构建一个简单的跑腿外卖小程序的原型&#xff0c;展示其基本功能和实现原理。 首先&…

外汇天眼:澳大利亚法院判决西太平洋银行因不合理行为在利率互换交易中支付180万美元

联邦法院宣布&#xff0c;西太平洋银行在2016年10月执行120亿美元的利率互换交易时涉及不合理行为。西太平洋银行将支付与该行为相关的最高罚款180万美元&#xff0c;以及澳大利亚证券投资委员会&#xff08;ASIC&#xff09;的1800万美元的诉讼和调查费用。 西太平洋银行的不…

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

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

awk命令以及用法

awk&#xff1a;是列处理工具&#xff0c;把一行提取出来&#xff0c;然后一列一列的对比&#xff0c;读取一行&#xff0c;处理一行。 sed&#xff1a;是行编辑器&#xff0c;无法取列&#xff0c;也是读取一行&#xff0c;处理一行。 -F 以什么为分隔符 {print $n} 打印第…

异步任务的一些思考

前言 XXL-Job部署教程 项目中&#xff0c;必然少不了数据的导入导出&#xff0c;针对数据的导入导出简单复盘一下。 为了不占用资源消耗时间&#xff0c;影响用户体验&#xff0c;大量数据的导入导出一般都是异步执行 导入的时候&#xff0c;如果数据量很大&#xff0c;一次…

腾讯云部署vue+node项目

文章目录 一、安装宝塔二、vue项目部署三、node项目部署 前言: 关于项目部署,一开始也是找了很多资料,费了点时间,所以记录一下。希望能对各位有所帮助。 一、安装宝塔 1.首先在控制台,进入云服务器的终端界面 2.输入命令和密码获取权限,并且安装宝塔界面 yum install -y w…