[单例模式]

[设计模式]

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

不同语言适用的设计模式是不一样的. 这里我们接下来要谈到的是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.mzph.cn/pingmian/59589.shtml

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

相关文章

内部知识库:优化企业培训流程的关键驱动力

在当今快速变化的商业环境中,企业培训的重要性日益凸显。内部知识库作为整合、管理和分享企业内部学习资源的关键工具,正逐步成为优化企业培训流程的核心。以下将探讨内部知识库如何通过多种功能,助力企业提升培训效率、质量和员工满意度。 …

Ubuntu - 进入紧急模式,无法进入桌面

目录 一、问题 二、分析原因 三、解决 四、参考 一、问题 重新安装VMVare之后,将之前的虚拟机加载不进来 二、分析原因 查看系统错误日志 journalctl -xb | grep Failed mnt挂载找不到了 三、解决 查看系统错误日志 如果是磁盘错误,此时终端会有…

RUP 统一软件开发过程

RUP(Rational Unified Process)是一种软件开发过程框架, Rational表示RUP是由Rational公司提出的,Unified表示RUP是最佳开发经验总结,而Process表示RUP是一个软件开发过程。RUP提供了一种结构化的方法来管理软件开发项…

I.MX6U 裸机开发3. GPIO操作控制LED灯

I.MX6U 裸机开发3. GPIO操作控制LED灯 一、创建项目目录及源文件1. 新建目录2. 远程开发环境3. 创建源文件 二、代码编写1. 打开时钟2. 配置端口复用功能为GPIO3. 配置端口电气属性4. 设置GPIO方向(GDIR寄存器)5. 输出6. 死循环等待 三、编译程序1. 整体…

ubuntu 22.04 server 安装 mysql 5.7.40 LTS

ubuntu 22.04 server 安装 mysql 5.7.40 LTS 参考: ubuntu 22.04 server 安装 和 初始化 LTS https://blog.csdn.net/wowocpp/article/details/143562451 centos7 安装 mysql5.7 LTS https://blog.csdn.net/wowocpp/article/details/139467094 mysql 常用指令 L…

java ssm 公司内部员工管理系统 员工信息管理 企业员工 源码 jsp

一、项目简介 本项目是一套基于SSM的公司内部员工管理系统,主要针对计算机相关专业的和需要项目实战练习的Java学习者。 包含:项目源码、数据库脚本、软件工具等。 项目都经过严格调试,确保可以运行! 二、技术实现 ​后端技术&am…

《Python使用sqlite3数据库》

《Python使用sqlite3数据库》 1、连接数据库2、创建游标3、执行SQL语句4、提交更改5、查询数据6、关闭连接 Python可以使用多种数据库,以下是一般步骤和示例: 1、连接数据库 首先要安装对应的数据库驱动。如使用MySQL数据库,要安装pymysql库…

数据分析:宏基因组DESeq2差异分析筛选差异物种

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍原理:计算步骤:结果:加载R包准备画图主题数据链接导入数据Differential abundance (No BP vs 2BP TA)构建`countData`矩阵过滤低丰度物种构建DESeq数据对象DESeq2差异分析画图Di…

Android的Handler

1. Handler是用于线程间通信,本质上是: Handler调用发送方法,向与Looper绑定的消息队列写入消息,然后Looper.loop()会循环的从消息队列里拿出消息。并调用dispatchMessage处理消息。而需要此消息的线程会实现回调的handleMessage…

GIF图片格式详解(三)

gif历史部分介绍请参考上一篇《GIF图片格式详解(一)》, 格式部分详解参考 《GIF图片格式详解(二)》 或直接访问博客地址:https://blog.whatsroot.xyz/2023/12/16/all-about-gif/ 本篇介绍下用于处理gif图…

Spark的yarn集群环境搭建

一.为什么要搭建yarn集群 为什么要将Spark的程序运行在YARN上,不运行在自带的 Standalone集群上? 1、统一化资源管理 Standalone是Spark专用的资源管理集群,只能用于运行 Spark程序 YARN是功能的分布式资源管理平台,可以运行各种分…

51单片机教程(六)- LED流水灯

1 项目分析 基于点亮LED灯、LED灯闪烁,扩展到构成最简单、花样流水灯。 2 技术准备 1 流水灯硬件及原理图 流水灯是由多个LED灯组成的 2 C语言知识点 数组 数组声明:长度不可变 数据类型 数组名称[长度n] // 整数型默认为0,小数型默认…

PyQt5实战——翻译的实现,第一次爬取微软翻译经验总结(八)

个人博客:苏三有春的博客 系类往期文章: PyQt5实战——多脚本集合包,前言与环境配置(一) PyQt5实战——多脚本集合包,UI以及工程布局(二) PyQt5实战——多脚本集合包,程序…

前端好用的网站分享——CSS(持续更新中)

1.CSS Scan 点击进入CSS Scan CSS盒子阴影大全 2.渐变背景 点击进入color.oulu 3.CSS简化压缩 点击进入toptal 4.CSS可视化 点击进入CSS可视化 这个强推,话不多说,看图! 5.Marko 点击进入Marko 有很多按钮样式 6.getwaves 点击进入getwaves 生…

理解Web登录机制:会话管理与跟踪技术解析(三)-过滤器Filter

在Java Web应用中,Filter(过滤器)是实现登录校验的常见方式。通过Filter,我们能够在请求到达实际的业务逻辑之前,对其进行拦截和处理,从而完成身份校验、权限验证等操作。本文将深入探讨登录校验的实现方法…

FreeBSD将操作系统支持时间从5年缩短为4年 继续与AMD合作

FreeBSD 项目今天发布了 2024 年第三季度进度报告,概述了该开源 BSD 操作系统在上一季度的改进情况。FreeBSD 开发人员仍然非常忙碌,他们在 2024 年第三季度取得的一些成就包括: FreeBSD 发布团队决定将支持时限从五年缩短为四年。 AMD 与 F…

kafka如何获取 topic 主题的列表?

大家好,我是锋哥。今天分享关于【kafka如何获取 topic 主题的列表?】面试题?希望对大家有帮助; kafka如何获取 topic 主题的列表? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在Kafka中,可以…

Java SPI机制简单讲解

前言 在Java开发中,经常会遇到需要扩展系统功能的需求。为了使系统更加灵活和可扩展,Java提供了SPI(Service Provider Interface)机制。本文将简单介绍SPI机制的基本概念、工作原理,并通过一个具体的示例来展示如何使…

【鉴权】OAuth 2.0: 高度灵活与安全的身份认证框架

目录 引言一、OAuth 2.0 的核心概念1.1 资源拥有者(Resource Owner)1.2 客户端(Client)1.3 授权服务器(Authorization Server)1.4 资源服务器(Resource Server)1.5 OAuth 2.0体系架构…

【网络-交换机】生成树协议、环路检测

路由优先级 路由优先级决定了在多种可达的路由类型中,哪种路由将被用来转发数据包。路由优先级值越低,对应路由的优先级越高,优先级值255表示对应的路由不可达。一般情况下,静态路由的优先级为1,OSPF路由优先级为110&a…