43 | 单例模式(下):如何设计实现一个集群环境下的分布式单例模式?
上两篇文章中,我们针对单例模式,讲解了单例的应用场景、几种常见的代码实现和存在的问题,并粗略给出了替换单例模式的方法,比如工厂模式、IOC 容器。今天,我们再进一步扩展延伸一下,一块讨论一下下面这几个问题:
- 如何理解单例模式中的唯一性?
- 如何实现线程唯一的单例?
- 如何实现集群环境下的单例?
- 如何实现一个多例模式?
今天的内容稍微有点“烧脑”,希望你在看的过程中多思考一下。话不多说,让我们正式开始今天的学习吧!
如何理解单例模式中的唯一性?
首先,我们重新看一下单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”
定义中提到,“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是什么呢?是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象?答案是后者,也就是说,单例模式创建的对象是进程唯一的。这里有点不好理解,我来详细地解释一下。
我们编写的代码,通过编译、链接,组织在一起,就构成了一个操作系统可以执行的文件,也就是我们平时所说的“可执行文件”(比如 Windows 下的 exe 文件)。可执行文件实际上就是代码被翻译成操作系统可理解的一组指令,你完全可以简单地理解为就是代码本身。
当我们使用命令行或者双击运行这个可执行文件的时候,操作系统会启动一个进程,将这个执行文件从磁盘加载到自己的进程地址空间(可以理解操作系统为进程分配的内存存储区,用来存储代码和数据)。接着,进程就一条一条地执行可执行文件中包含的代码。比如,当进程读到代码中的 User user = new User(); 这条语句的时候,它就在自己的地址空间中创建一个 user 临时变量和一个 User 对象。
进程之间是不共享地址空间的,如果我们在一个进程中创建另外一个进程(比如,代码中有一个 fork() 语句,进程执行到这条语句的时候会创建一个新的进程),操作系统会给新进程分配新的地址空间,并且将老进程地址空间的所有内容,重新拷贝一份到新进程的地址空间中,这些内容包括代码、数据(比如 user 临时变量、User 对象)。
所以,单例类在老进程中存在且只能存在一个对象,在新进程中也会存在且只能存在一个对象。而且,这两个对象并不是同一个对象,这也就说,单例类中对象的唯一性的作用范围是进程内的,在进程间是不唯一的。
如何实现线程唯一的单例?
刚刚我们讲了单例类对象是进程唯一的,一个进程只能有一个单例对象。那如何实现一个线程唯一的单例呢?
我们先来看一下,什么是线程唯一的单例,以及“线程唯一”和“进程唯一”的区别。
“进程唯一”指的是进程内唯一,进程间不唯一。类比一下,“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一”还代表了线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。这段话听起来有点像绕口令,我举个例子来解释一下。
假设 IdGenerator 是一个线程唯一的单例类。在线程 A 内,我们可以创建一个单例对象 a。因为线程内唯一,在线程 A 内就不能再创建新的 IdGenerator 对象了,而线程间可以不唯一,所以,在另外一个线程 B 内,我们还可以重新创建一个新的单例对象 b。
尽管概念理解起来比较复杂,但线程唯一单例的代码实现很简单,如下所示。在代码中,我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。
public class IdGenerator {private AtomicLong id = new AtomicLong(0);private static final ConcurrentHashMap<Long, IdGenerator> instances= new ConcurrentHashMap<>();private IdGenerator() {}public static IdGenerator getInstance() {Long currentThreadId = Thread.currentThread().getId();instances.putIfAbsent(currentThreadId, new IdGenerator());return instances.get(currentThreadId);}public long getId() {return id.incrementAndGet();}
}
如何实现集群环境下的单例?
刚刚我们讲了“进程唯一”的单例和“线程唯一”的单例,现在,我们再来看下,“集群唯一”的单例。
首先,我们还是先来解释一下,什么是“集群唯一”的单例。
我们还是将它跟“进程唯一”“线程唯一”做个对比。“进程唯一”指的是进程内唯一、进程间不唯一。“线程唯一”指的是线程内唯一、线程间不唯一。集群相当于多个进程构成的一个集合,“集群唯一”就相当于是进程内唯一、进程间也唯一。也就是说,不同的进程间共享同一个对象,不能创建同一个类的多个对象。
我们知道,经典的单例模式是进程内唯一的,那如何实现一个进程间也唯一的单例呢?如果严格按照不同的进程间共享同一个对象来实现,那集群唯一的单例实现起来就有点难度了。
具体来说,我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
按照这个思路,我用伪代码实现了一下这个过程,具体如下所示:
public class IdGenerator {private AtomicLong id = new AtomicLong(0);private static IdGenerator instance;private static SharedObjectStorage storage = FileSharedObjectStorage(/*入参省略,比如文件地址*/);private static DistributedLock lock = new DistributedLock();private IdGenerator() {}public synchronized static IdGenerator getInstance() if (instance == null) {lock.lock();instance = storage.load(IdGenerator.class);}return instance;}public synchroinzed void freeInstance() {storage.save(this, IdGeneator.class);instance = null; //释放对象lock.unlock();}public long getId() { return id.incrementAndGet();}
}// IdGenerator使用举例
IdGenerator idGeneator = IdGenerator.getInstance();
long id = idGenerator.getId();
idGenerator.freeInstance();
如何实现一个多例模式?
跟单例模式概念相对应的还有一个多例模式。那如何实现一个多例模式呢?
“单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。如果用代码来简单示例一下的话,就是下面这个样子:
public class BackendServer {private long serverNo;private String serverAddress;private static final int SERVER_COUNT = 3;private static final Map<Long, BackendServer> serverInstances = new HashMap<>();static {serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));}private BackendServer(long serverNo, String serverAddress) {this.serverNo = serverNo;this.serverAddress = serverAddress;}public BackendServer getInstance(long serverNo) {return serverInstances.get(serverNo);}public BackendServer getRandomInstance() {Random r = new Random();int no = r.nextInt(SERVER_COUNT)+1;return serverInstances.get(no);}
}
实际上,对于多例模式,还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。这里的“类型”如何理解呢?
我们还是通过一个例子来解释一下,具体代码如下所示。在代码中,logger name 就是刚刚说的“类型”,同一个 logger name 获取到的对象实例是相同的,不同的 logger name 获取到的对象实例是不同的。
public class Logger {private static final ConcurrentHashMap<String, Logger> instances= new ConcurrentHashMap<>();private Logger() {}public static Logger getInstance(String loggerName) {instances.putIfAbsent(loggerName, new Logger());return instances.get(loggerName);}public void log() {//...}
}//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");
这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象,关于这一点,下一篇文章中就会讲到。实际上,它还有点类似享元模式,两者的区别等到我们讲到享元模式的时候再来分析。除此之外,实际上,枚举类型也相当于多例模式,一个类型只能对应一个对象,一个类可以创建多个对象。
重点回顾
好了,今天的内容到此就讲完了。我们来一块总结回顾一下,你需要掌握的重点内容。
今天的内容比较偏理论,在实际的项目开发中,没有太多的应用。讲解的目的,主要还是拓展你的思路,锻炼你的逻辑思维能力,加深你对单例的认识。
1. 如何理解单例模式的唯一性?
单例类中对象的唯一性的作用范围是“进程唯一”的。“进程唯一”指的是进程内唯一,进程间不唯一;“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一”就意味着线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。“集群唯一”指的是进程内唯一、进程间也唯一。
2. 如何实现线程唯一的单例?
我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 并发工具类,可以更加轻松地实现线程唯一单例。
3. 如何实现集群环境下的单例?
我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,需要显式地将对象从内存中删除,并且释放对对象的加锁。
4. 如何实现一个多例模式?
“单例”指的是一个类只能创建一个对象。对应地,“多例”指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。多例的实现也比较简单,通过一个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。
相关文章:
43 | 单例模式(下):如何设计实现一个集群环境下的分布式单例模式?
上两篇文章中,我们针对单例模式,讲解了单例的应用场景、几种常见的代码实现和存在的问题,并粗略给出了替换单例模式的方法,比如工厂模式、IOC 容器。今天,我们再进一步扩展延伸一下,一块讨论一下下面这几个…...
PHP如何解决异常处理
在PHP中,异常处理是通过使用try、catch、throw以及finally这几个关键字来实现的。以下是一个简单的介绍和示例: 异常处理的基本步骤 抛出异常: 使用throw关键字抛出一个异常对象。异常对象通常是Exception类或其子类的实例。 捕获异常&…...
C++ socket编程(3)
前面文章,介绍了一个简单socket通讯Demo, 客户端和服务器进行简单的交互。两个代码都很简单,如果情况一复杂,就会出错。这节我们把代码完善一下,实现一个客户端输入,发送,服务器echo的交互。本文…...
Collection-LinkedList源码解析
文章目录 概述LinkedList实现底层数据结构构造函数getFirst(), getLast()removeFirst(), removeLast(), remove(e), remove(index)add()addAll()clear()Positional Access 方法查找操作 概述 LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序…...
vue判断对象数组里是否有重复数据
TOCvue判断对象数组里是否有重复数据 try {//通过产品编码赛选出新的数组 在比较let names this.goodsJson.map(item > item["productCode"]);let nameSet new Set(names)if (nameSet.size ! names.length) {this.$message({message: 警告!产品选项…...
CSS 3D转换
在 CSS 中,除了可以对页面中的元素进行 2D 转换外,您也可以对象元素进行 3D转换(将页面看作是一个三维空间来对页面中的元素进行移动、旋转、缩放和倾斜等操作)。与 2D 转换相同,3D 转换同样不会影响周围的元素&#x…...
51单片机数码管循环显示0~f
原理图: #include <reg52.h>sbit dulaP2^6;//段选信号 sbit welaP2^7;//位选信号unsigned char num;//数码管显示的数字0~funsigned char code table[]{ 0x3f,0x06,0x5b,0x4f, 0x66,0x6d,0x7d,0x07, 0x7f,0x6f,0x77,0x7c, 0x39,0x5e,0x79,0x71};//定义数码管显…...
【编程进阶知识】Java NIO:掌握高效的I/O多路复用技术
Java NIO:掌握高效的I/O多路复用技术 摘要: 本文将带你深入了解Java NIO(New I/O)中的Selector类,探索如何利用它实现高效的I/O多路复用,类似于Linux中的select和epoll系统调用。文章将提供详细的代码示例…...
vscode创建flutter项目,运行flutter项目
打开View(查看) > Command Palette...(命令面板)。 可以按下 Ctrl / Cmd Shift P 输入 flutter 选择Flutter: New Project 命令 按下 Enter 。选择Application 选择项目地址 输入项目名称 。按下 Enter 等待项目初始化完成 …...
STM32之CAN外设
相信大家在学习STM32系列的单片机时,在翻阅芯片的数据手册时,都会看到这么一个寄存器外设——CAN外设寄存器。那么,大家知道这个外设的工作原理以及该如何使用吗?这节的内容将会详细介绍STM32上的CAN外设,文章结尾附有…...
【阅读笔记】水果轻微损伤的无损检测技术应用
一、水果轻微损伤检测技术以及应用 无损检测技术顾名思义就是指在不破坏水果样品完整性的情况下对样品进行品质鉴定。目前比较常用的农产品水果类无损检测法有:基于红外热成像、机器视觉技术的图像处理方法、光谱检测技术、介电特性技术检测法等。 1.1 基于红外热…...
忘记7-zip密码,如何解压文件?
7z压缩包设置了密码,解压的时候就需要输入正确对密码才能顺利解压出文件,正常当我们解压文件或者删除密码的时候,虽然方法多,但是都需要输入正确的密码才能完成。忘记密码就无法进行操作。 那么,忘记了7z压缩包的密码…...
SpringBoot基础(一)
1.SpringBoot简介 Spring Boot是Spring社区发布的一个开源项目,旨在帮助开发者快速并且更简单的构建项目。它 使用习惯优于配置的理念让你的项目快速运行起来,使用Spring Boot很容易创建一个独立运行 (运行jar,内置Servlet容器&am…...
Java智能匹配灵活用工高效人力资源管理系统小程序源码
智能匹配灵活用工高效人力资源管理系统 💼🚀 🚀 开篇:职场新风尚,智能匹配引领变革 在这个瞬息万变的时代,职场也在经历着前所未有的变革。传统的用工模式已难以满足现代企业的需求,而“智能匹…...
openpdf
1、简介 2、示例 2.1 引入依赖 <dependency><groupId>com.github.librepdf</groupId><artifactId>openpdf</artifactId><version>1.3.34</version></dependency><dependency><groupId>com.github.librepdf</…...
C#垃圾回收机制详解
本文详解C#垃圾回收机制。 目录 一、C#垃圾收集器定义 二、C#中的垃圾收集器特点 三、垃圾回收触发条件 四、常见的内存泄漏情况 五、高性能应用程序的垃圾回收策略 六、最佳实践和建议 七、实例 一、C#垃圾收集器定义 int、string变量,这些数据都存储在内存中,如果…...
身份证二要素核验操作指南
身份证二要素核验主要涉及验证身份证上的姓名和身份证号码这两个关键信息,以下是详细的操作指南: 一、核验流程 输入信息:用户在客户端(如APP、网站等)输入自己的姓名和身份证号码。 信息加密与传输:客户端…...
量子数字签名概述
我们都知道,基于量子力学原理研究密钥生成和使用的学科称为量子密码学。其内容包括了量子密钥分发、量子秘密共享、量子指纹识别、量子比特承诺、量子货币、秘密通信扩展量子密钥、量子安全计算、量子数字签名、量子隐性传态等。虽然各种技术发展的状态不同…...
算法题——合并 k 个升序的链表
题目描述: 合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。 数据范围:节点总数 0≤n≤50000≤n≤5000,每个节点的val满足 ∣val∣<1000∣val∣<1000 要求:时间复杂度 O(nlogn) 一、常见解法 (…...
智能制造与精益制造的模型搭建
现行制造模式分析I-痛点改善思路-管控省优四化推行...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...
STM32标准库-DMA直接存储器存取
文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA(Direct Memory Access)直接存储器存取 DMA可以提供外设…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
算法笔记2
1.字符串拼接最好用StringBuilder,不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格...
C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...
