当前位置: 首页 > news >正文

【设计模式】单例模式的前世今生

文章目录

    • 引言
    • 简介
    • 起航!向“确保某个类在系统中只有一个实例”进发 ⛵️
      • Lazy Singleton
      • Double-checked locking(DCL) Singleton
      • Volatile Singleton
      • Atomic Singleton
      • Meyers Singleton
    • 附:C++静态对象的初始化

引言

说起单例模式,我想,即便屏幕前的你此前没有系统学习过设计模式,也应该听说过它的大名。

但是,这篇文章的重点不是去聊这个模式在实际生产过程中怎么用,而是想聊一下这个模式发展的历史。如果你的目的是想了解其具体用法,你可以在检索一下其他人写的总结,再往下看的话,可能不会有你想要的答案。

简介

在软件系统中,经常有一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性以及良好的效率。

单例模式是一种设计模式,其核心目的是确保某个类在系统中只有一个实例,并提供一个全局访问点来访问这个实例。

“确保某个类在系统中只有一个实例”——这个目的听起来似乎很简单,不要觉得荒谬,某些特定的情况下,我们的系统中确实只需要某个类的一个实例就可以了,这样既能满足实际使用场景,又能减少内存开销,避免资源的多重占用,提升性能。

倘若我们从这个目的出发——“确保某个类在系统中只有一个实例”,现在的任务就是:设计某种手段以达到我们的目的。

起航!向“确保某个类在系统中只有一个实例”进发 ⛵️

也许,刚看到这个目标的时候你会有点疑惑:这不是很简单吗?既然你想要确保系统中只有一个某个类的对象,那我就只创建一个对象不就好了吗?

听起来好像没错,但是“确保某个类在系统中只有一个实例”,这应该是类设计者的责任,而不是使用者的责任。

现在,让我们从类设计者的角度重新审视这个问题。

我们知道,创建类的实例——这个动作是借由类的构造函数完成的,换句话说,我们可以确定问题的突破点是在构造函数身上。那么,如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例呢?

首先,我们先解决构造函数的权限问题。C++中的权限说起来一共有三种:public,protect,private。而无论对于用户还是派生类来讲,真正的权限事实上只有两种:

  • 对于用户而言,public权限是可访问的,private权限和protect权限是不可访问的;
  • 对于派生类而言,private是不可访问的,protect与public是可访问的;

而如果将这个类的构造函数用public去修饰,意味着用户可以随意创建对象,“创建对象”这个动作无法受到我们的管控,因此,如果想要限制用户“不那么自由”的创建实例,我们应当将构造函数声明为private:

class Singleton{private:Singleton();//私有构造函数static Singleton* m_instance;public:static Singleton* getInstance();//全局访问点
}
Singleton* Singleton::m_instance = NULL;

Lazy Singleton

那么如何“确保某个类在系统中只有一个实例”?很容易想到:

1 Singleton* Singleton::getInstance(){
2  if(m_instance == nullptr){
3    	m_instance = new Singleton();
4  }
5  return m_instance;
6 }

懒汉版(Lazy Singleton):单例实例在第一次被使用时才进行初始化,这叫做延迟初始化,也叫做懒加载。

Lazy Singleton存在内存泄露的问题,这里有两种解决方法:

  1. 使用智能指针
  2. 使用静态的嵌套类对象

对于第二种解决方法,代码如下:

// version 1.1
class Singleton
{
private:static Singleton* instance;
private:Singleton() { };~Singleton() { };Singleton(const Singleton&);Singleton& operator=(const Singleton&);
private:class Deletor {public:~Deletor() {if(Singleton::instance != NULL)delete Singleton::instance;}};static Deletor deletor;
public:static Singleton* getInstance() {if(instance == NULL) {instance = new Singleton();}return instance;}
};// init static member
Singleton* Singleton::instance = NULL;

在程序运行结束时,系统会调用静态成员deletor的析构函数,该析构函数会删除单例的唯一实例。使用这种方法释放单例对象有以下特征:

  • 在单例类内部定义专有的嵌套类。
  • 在单例类内定义私有的专门用于释放的静态成员。
  • 利用程序在结束时析构全局变量的特性,选择最终的释放时机。

这是一个简单的实现版本,”有条件“ 的完成了我们的目标,因为这个版本只能针对于单线程下的程序,是个“线程非安全”版本,一旦线程数大于1,这个版本将不再起作用。

假设现在有两个线程:thread A与thread B。

thread A 执行完第2行,还没来得及执行第3行时,thread B 抢到了时间片,由于此时的m_instance仍为空,因此thread也能进入if分支,然后m_instance就被创建了两次。

有没有什么办法能够快速修复这个“bug“呢?

Double-checked locking(DCL) Singleton

很自然的,你会想到加锁:

1 Singleton* Singleton::getInstance(){
2  Lock lock;
3  if(m_instance == nullptr){
4    	m_instance = new Singleton();
5  }
6  return m_instance;
7 }

如你所愿,我们在这个版本里加了一个锁,再遇到上述场景时,由于thread A抢到了锁并且还没释放,因此,thread A能正常创建实例,并且当thread A出了函数体释放了锁之后,thread B 进入函数体,由于此时m_instance已经被创建,因此并不会被创建两次。

问题解决了吗?

按照上面的分析,好像是的。但是,你有没有注意到当实例已经被创建后的场景?

假设实例m_instance已经被创建,在之后的场景中,程序再次进入该函数时,都会先创建锁,然后判断m_instance是否为空,然后返回。每次进入函数体都会创建锁,但是这个锁只有第一次才有真正的作用,之后都是在浪费资源。

这个版本能够保证线程安全,但是锁的代价过高。

还有没有改进版本呢?

于是,双检查锁版本诞生了:

1 Singleton* Singleton::getInstance(){
2 if(m_instance == nullptr){
3  	Lock lock; 基于作用域的加锁,超出作用域,自动调用析构函数解锁
4  	if(m_instance == nullptr){
5    		m_instance = new Singleton();
6  	}
7 }
8  	return m_instance;
9 }

之前的版本是不管三七二十一,都加锁,现在的版本是进入函数体之后,先问一次m_instance是不是空,根据结果去决定是否加锁。规避了上一个版本锁的代价过高的问题。

有的小伙伴可能会在这里犯迷糊:认为第二个if分支没有必要,即可以删去第4行。

事实上,如果删去了第4行,那么情况就会变得跟第一个版本一模一样,只要线程能同时通过第2行的检查,那么这个实例就有被创建多次的可能。就算此时加了这个锁,无非也就是多等一会儿,没有其他作用。

这个版本看起来很完美,问题似乎已经被我们解决了!

但是我要告诉你,这个版本在很长一段时间内迷惑了很多人,包括一些专家都认为这个版本已经达到目标了。直到2000年左右,Java领域的某些研究者才发现有问题,而且很快在几乎所有的语言领域都发现这种实现有漏洞。由于内存读写reorder不安全,会导致双检查锁失效。

怎么样的一个失效问题呢?

让我们将目光聚焦到这行代码上:

m_instance = new Singleton();

这行代码最终会被编译器编译成一段指令序列,线程是在指令层次抢时间片的。但是这个指令有时候跟我们的假设不一样。

比如上面那行代码通常情况下到了指令层次之后,可以划分为三个动作:

  1. 分配一片内存;
  2. 在这片内存上执行初始化操作;
  3. 将得到的内存地址赋值给m_instance;

是这三个动作没错,但是到了指令层面之后,它们的顺序却可能由于编译器优化而被打乱成下面这样:

  1. 分配一片内存;
  2. 将最后得到的内存地址赋值给m_instance;
  3. 在这片内存上执行初始化操作;

看到了吗?第二步和第三步的顺序可能会被颠倒!

1 Singleton* Singleton::getInstance(){
2 if(m_instance == nullptr){
3  	Lock lock;
4  	if(m_instance == nullptr){
5    		m_instance = new Singleton();
6  	}
7 }
8  	return m_instance;
9 }

现在再次回到之前的场景,假设有两个thread,thread A执行第5步之后,由于编译器优化而执行了:

  1. 分配一片内存;
  2. 将最后得到的内存地址赋值给m_instance;

第三步还没来得及执行,时间片就被thread B抢走了,由于此时m_instance已经被赋予了地址,因此m_instance不再为空!当thread B再次进入函数体之后,由于第2步判断m_instance是否为空的结果为false,导致被直接返回。而事实上m_instance并没有完成初始化操作,此时还不能使用。

当这个问题被发现后,由于是编译器优化导致了此类问题的出现,于是人们敦促编译器厂商给出问题解决方案。

Volatile Singleton

反过来想想,编译器优化的目的是提升程序性能,只是不巧导致了这个问题的出现,如果为了一个单例模式的实现直接禁止这种优化,属实有点说不过去。这个时候java和C#就很聪明,在各自的语言中加了一个关键字:Volatile,其作用也很直截了当:禁止指令重排。

C++呢?Visual C++嫌标准委员会动作太慢,2005年左右,在自家编译器里也加入了volatile关键字,但是由于是个人行为,很显然不能跨平台。之后C++11正式将volatile作为关键字纳入标准:

class Singleton {  
public:  static Singleton* instance() {  if (pInstance == 0) {  Lock lock;  if (pInstance == 0) {  pInstance = new Singleton;  }  }  return pInstance;  }  
private:  static Singleton * volatile pInstance;  Singleton(){  }  
};  

volatile这个关键字有两层语义:

第一层语义是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中,即看到的都是最新的结果。

第二层语义是禁止指令重排序优化。我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。

Atomic Singleton

另外在C++11 将原子操作纳入了标准,我们可以通过标准提供的原子操作来处理该问题。

通过给原子变量设置 std::std::memory_order_xxx 来防止 CPU 的指令重排操作。

//C++11版本之后的跨平台实现(volatile)std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;Singleton* Singleton::getInstance(){Singleton* tmp = m_instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);//获取内存fenceif(tmp == nullptr){std::lock_guard<std::mutex> lock(m_mutex);tmp = m_instance.load(std::memory_order_relaxed);if(tmp == nullptr){tmp = new Singleton;std::atomic_thread_fence(std::memory_order_relaced);//释放内存fencem_instance.store(tmp,std::memory_order_relaxed);}}return tmp;
}

Meyers Singleton

《Effective C++》的作者Meyer,在<<Effective C++>>3rd Item4中,提出了一种到目前为止最简洁高效的解决方案:

template<typename T>
class Singleton
{
public:static T& getInstance(){static T value;return value;}private:Singleton();~Singleton();
};

非常优雅的一种实现。

先说结论:

  • 单线程下,正确。
  • C++11及以后的版本(如C++14)的多线程下,正确。
  • C++11之前的多线程下,不一定正确。

原因在于在C++11之前的标准中并没有规定local static变量的内存模型,所以很多编译器在实现local static变量的时候仅仅是进行了一次check(参考《深入探索C++对象模型》),于是getInstance函数被编译器改写成这样了:

bool initialized = false;
char value[sizeof(T)];T& getInstance()
{if (!initialized){initialized = true;new (value) T();}return *(reinterpret_cast<T*>(value));
}

于是乎它就是不是线程安全的了。

但是在C++11却是线程安全的,这是因为新的C++标准规定了当一个线程正在初始化一个变量的时候,其他线程必须得等到该初始化完成以后才能访问它。

附:C++静态对象的初始化

non-local static对象(函数外)

C++规定,non-local static 对象的初始化发生在main函数执行之前,也即main函数之前的单线程启动阶段,所以不存在线程安全问题。但C++没有规定多个non-local static 对象的初始化顺序,尤其是来自多个编译单元的non-local static对象,他们的初始化顺序是随机的。

local static 对象(函数内)

对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。

在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。

而C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。

相关文章:

【设计模式】单例模式的前世今生

文章目录 引言简介起航&#xff01;向“确保某个类在系统中只有一个实例”进发 ⛵️Lazy SingletonDouble-checked locking&#xff08;DCL&#xff09; SingletonVolatile SingletonAtomic SingletonMeyers Singleton 附&#xff1a;C静态对象的初始化 引言 说起单例模式&…...

厦门网上在线教育系统,线下老师怎么转型到线上网上授课?

现在很多 线下老师都想转到线上做网课&#xff0c;但是在转线上过程中会出现很多问题&#xff0c;很多人都不知道怎么开始&#xff0c;今天小编和大家说一下要注意的。 一、你要有一套适合线上的教学视频 首先你要准备做的课程内容是什么&#xff0c;怎么讲&#xff0c;讲什么&…...

Spring底层入门(九)

boot的执行流程分为构造SpringApplication对象、调用run方法两部分 1、Spring Boot 执行流程-构造 通常我们会在SpringBoot的主启动类中写以下的代码&#xff1a; 参数一是当前类的字节码&#xff0c;参数二是main的args参数。 public class StartApplication {public static…...

掌握Android Fragment开发之魂:Fragment的深度解析(下)

在上一篇文章中&#xff0c;我们深入探讨了Fragment 通信&#xff0c;包含Fragment 向 Activity 传递数据、Activity 向 Fragment 传递数据、Fragment 之间的通信方式。感兴趣的朋友&#xff0c;请前往查阅&#xff1a; 掌握Android Fragment开发之魂&#xff1a;Fragment的深度…...

小巧简单实用的Linux端口转发工具Rinetd

Linux下实现端口转发有很多种方法&#xff0c;尤其是在可以联网的情况下&#xff0c;更是容易。最近在资源受限的定制系统中&#xff0c;找到一个方便离线安装和使用的端口转发工具Rinetd&#xff0c;安装包仅几十K&#xff0c;而且有很多版本的Linux发行系统的支持。 1、安装…...

HackBar 新手使用教程(入门)

啥是Hackbar&#xff1f; Hackbar是一个Firefox 的插件,它的功能类似于地址栏,但是它里面的数据不受服务器的相应触发的重定向等其它变化的影响。 有网址的载入于访问,联合查询,各种编码,数据加密功能。 这个Hackbar可以帮助你在测试SQL注入,XSS漏洞和网站的安全性,主要是帮助…...

<Linux> 权限

目录 权限人员相对于文件来说的分类更改权限文件的拥有者与所属组umask粘滞位 权限 权限是操作系统用来限制对资源访问的机制&#xff0c;权限一般分为读、写、执行。系统中的每个文件都拥有特定的权限、所属用户及所属组&#xff0c;通过这样的机制来限制哪些用户、哪些组可以…...

Nacos Docker 快速部署----解决nacos鉴权漏洞问题

Nacos Docker 快速部署 1. 说明 1.1 官方文档 官方地址 https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html docker启动文件的gitlhub地址 https://github.com/nacos-group/nacos-docker.git 问题&#xff1a; 缺少部分必要配置与说明 1.2 部署最新版本Nacos&…...

存储或读取时转换JSON数据

一、 数据库类型 二、使用Hutool工具 存储时将数据转换为JSON数据 获取时将JSON数据转换为对象 发现问题&#xff1a; 原本数据对象是Address 和 Firend但是转换完成后数据变成了JSONArray和JSONObject 三、自定义TypeHandler继承Mybatis的BaseTypeHandler处理器 package …...

Spring Data JPA的作用和用法

Spring Data JPA 是 Spring 框架的一个模块&#xff0c;它提供了一种数据访问抽象&#xff0c;允许以一种声明式和简洁的方式来处理数据库操作。它基于 Java Persistence API (JPA)&#xff0c;是一个行业标准的 ORM&#xff08;对象关系映射&#xff09;规范&#xff0c;用于将…...

【go项目01_学习记录08】

学习记录 1 模板文件1.1 articlesStoreHandler() 使用模板文件1.2 统一模板 1 模板文件 重构 articlesCreateHandler() 和 articlesStoreHandler() 函数&#xff0c;将 HTML 抽离并放置于独立的模板文件中。 1.1 articlesStoreHandler() 使用模板文件 . . . func articlesSt…...

Java中的线程

一、创建线程的几种方式&#xff1f; ① 通过继承Thread类并重写run方法 &#xff0c;实现简单但不可以继承其他类 Thread底层也是实现了Runnable接口&#xff0c;重写的是run而不是start方法 ②实现Runnable接口并重写run方法&#xff0c; 避免了单继承的局限性&#xff…...

顺序表的实现(迈入数据结构的大门)(完整代码)

seqlist.h #pragma once typedef int SLDataType;#include<stdio.h> #include<stdlib.h> #include<assert.h>typedef struct SeqList {SLDataType* a;int size; // 有效数据个数int capacity; // 空间容量 }SL;//初始化和销毁 void SLInit(SL* ps); void SL…...

neo4j-5.11.0安装APOC插件or配置允许使用过程的权限

在已经安装好neo4j和jdk的情况下安装apoc组件&#xff0c;之前使用neo4j-community-4.4.30&#xff0c;可以找到配置apoc-4.4.0.22-all.jar&#xff0c;但是高版本neo4j对应没有apoc-X.X.X-all.jar。解决如下所示&#xff1a; 1.安装好JDK与neo4j 已经安装对应版本的JDK 17.0…...

mybatis 中 #{}和 ${}的区别是什么?

在 MyBatis 中&#xff0c;#{} 和 ${} 是两种用于参数替换的语法&#xff0c;但它们之间存在一些重要的区别&#xff0c;主要体现在安全性、预编译和动态 SQL 上。 安全性&#xff1a; #{}&#xff1a;这是预编译处理&#xff0c;MyBatis 会为传入的参数生成 PreparedStatement…...

深入解析C#中的接口设计原则

深入解析C#中的接口设计原则 目录 深入解析C#中的接口设计原则 一、接口设计的SOLID原则 二、接口设计的最佳实践 三、接口设计的高级技术 四、结论 接口在面向对象编程中扮演着至关重要的角色。它们是定义行为契约的一种方式&#xff0c;允许实现者提供这些行为的具体实现…...

106短信群发平台在金融和法务行业的应用分析

一、金融行业应用 1.客户通知与提醒&#xff1a;银行、证券、保险等金融机构经常需要向客户发送各类通知和提醒&#xff0c;如账户余额变动、交易确认、扣费通知、理财产品到期提醒等。106短信群发平台可以快速、准确地将这些信息发送到客户的手机上&#xff0c;确保客户及时获…...

Spring AOP(2)

目录 Spring AOP详解 PointCut 切面优先级Order 切点表达式 execution表达式 切点表达式示例 annotation 自定义注解MyAspect 切面类 添加自定义注解 Spring AOP详解 PointCut 上面代码存在一个问题, 就是对于excution(* com.example.demo.controller.*.*(..))的大量重…...

Spring-依赖注入的处理过程

前置知识 1 入口 DefaultListableBeanFactory#resolveDependency 2 每个依赖都有对应的DependencyDescriptor 3 自定绑定候选对象处理器AutowireCapableBeanFactory 注入处理 我们可以看到接口AutowireCapableBeanFactory中有两个方法。 第一个是单个注入&#xff1a; Null…...

2.用python爬取的保存在text文件中的格式为MP4的视频url

文章目录 一、url的保存格式二、MP4视频获取 一、url的保存格式 爬取的视频名字和url保存在text文件中&#xff0c;每一个视频都是一个单独的text&#xff0c;其中text的文件名就是视频的名字&#xff0c;text内容是视频的下载url&#xff0c;并且所有的text都保存在同一个文件…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…...

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…...

超短脉冲激光自聚焦效应

前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应&#xff0c;这是一种非线性光学现象&#xff0c;主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场&#xff0c;对材料产生非线性响应&#xff0c;可能…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现

摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序&#xff0c;以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务&#xff0c;提供稳定高效的数据处理与业务逻辑支持&#xff1b;利用 uniapp 实现跨平台前…...

ServerTrust 并非唯一

NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

关于 WASM:1. WASM 基础原理

一、WASM 简介 1.1 WebAssembly 是什么&#xff1f; WebAssembly&#xff08;WASM&#xff09; 是一种能在现代浏览器中高效运行的二进制指令格式&#xff0c;它不是传统的编程语言&#xff0c;而是一种 低级字节码格式&#xff0c;可由高级语言&#xff08;如 C、C、Rust&am…...

什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南

文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/55aefaea8a9f477e86d065227851fe3d.pn…...

【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分

一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计&#xff0c;提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合&#xff1a;各模块职责清晰&#xff0c;便于独立开发…...

Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信

文章目录 Linux C语言网络编程详细入门教程&#xff1a;如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket&#xff08;服务端和客户端都要&#xff09;2. 绑定本地地址和端口&#x…...