一文搞懂设计模式之单例模式

大家好,我是晴天,本周我们一起来学习单例模式。本文将介绍单例模式的基本属性,两种构造单例的方法(饿汉模式和懒汉模式)以及golang自带的sync.Once()方法。

一文搞懂设计模式之单例模式.png

什么是单例模式

GoF对单例模式的定义是:保证一个类、只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。

单例模式属于创建型设计模式,单例模式能够保证一个类全局只有唯一一个实例对象。

单例模式类图.png

为什么需要单例模式

在以下几种场景下,建议使用单例模式:

  1. 某些全局资源进行共享时,需要使用唯一的对象进行访问
  2. 某些实例化很费时的操作,只进行一次实例化
  3. 某些入参特别复杂的模块或者函数,只用一个实例化对象操作

单例模式的分类

  • 饿汉模式:特点是在类加载的时候就创建实例,而不是在实际使用时再进行实例化
  • 懒汉模式:特点是在实际使用的时候才进行实例化,创建实例

饿汉模式

单例创建步骤.drawio.png

饿汉模式,顾名思义,就是无论是否需要这个单例对象,都在程序运行时,创建这个对象,“饥饿疗法”。我们来看一下常规的一个饿汉模式的写法。

package mainimport "fmt"// 单例模式要点:
/*1.某个类只能有一个实例2.该类必须自己创建这个实例3.该类必须给所有其他对象提供这个实例综述:保证一个类全局只能有一个实例对象,并提供一个全局访问点
*/// 1.单例类需要是包内私有的,不能被外界访问到,否则就能实例化多个对象
type singletonCar struct {name string
}// 2.访问单例对象的指针必须是私有指针,不能被外界访问到,否则外界就能修改这个指针的指向,导致单例对象丢失
var sc *singletonCar// 饿汉模式
// 系统启动时就创建单例对象,无论后续是否需要
func init() {sc = newSingletonCar()
}func newSingletonCar() *singletonCar {return &singletonCar{"BMW"}
}// 3.对外提供的全局访问点,包外能够获得这个单例对象,只提供读权限,不提供写权限
func GetSingleCar() *singletonCar {return sc
}// GetSingleCar这个方法只能是普通全局函数,不能是单例类的成员函数
// 以下注释写法是错误的,因为无法获取到单例对象,也就无法调用获取单例对象的函数
//
//  func (sc *singletonCar) GetSingleton() *singletonCar {
//     return sc
//  }func (sc *singletonCar) PrintCarName() {fmt.Println(sc.name)
}func main() {singleCar := GetSingleCar()singleCar.PrintCarName() // BMWsingleCar2 := GetSingleCar()singleCar2.PrintCarName() // BMWfmt.Println(singleCar == singleCar2) //true
}
代码解析:
  1. 第一步声明一个单例类singletonCar
  2. 第二步声明一个指向单例对象的指针,并初始化单例对象
  3. 第三步对外提供一个全局访问函数GetSingleton来获取这个单例对象
问题讨论:

上述代码逻辑上看起来没什么问题,能正常运行。但是在实践过程中,发现了一个问题,获取到的这个单例对象,是没有办法作为其他函数的入参或者出参的,因为包外无法拿到这个单例对象的类型。

改进:

为了解决上述问题,可以给单例类封装一个接口,让包外以接口的形式访问这个单例。只需要对代码稍作调整即可。

type SingletonCarInterface interface {PrintCarName()
}// 3.对外提供的全局访问点,包外能够获得这个单例对象,只提供读权限,不提供写权限
func GetSingleCar() SingletonCarInterface {return sc
}

懒汉模式

懒汉模式.drawio.png

懒汉模式,顾名思义就是在第一次获取单例对象的时候,才进行实例化。我们来看一下懒汉模式第一个版本的代码。

package mainimport "fmt"// 1.单例类需要是包内私有的,不能被外界访问到,否则就能实例化多个对象
type singletonCar struct {name string
}type SingletonCarInterface interface {PrintName()
}// 2.访问单例对象的指针必须是私有指针,不能被外界访问到,否则外界就能修改这个指针的指向,导致单例对象丢失
var s *singletonCarfunc newSingletonCar() *singletonCar {return &singletonCar{name: "BMW",}
}func (sc *singletonCar) PrintName() {fmt.Println(sc.name)
}// 懒汉模式在获取对象的时候才会实例化对象
func GetSingleton() SingletonCarInterface {// 是第一次获取对象if s == nil {s = newSingletonCar()}return s
}func main() {sc := GetSingleton()sc.PrintName()
}
代码解析:
  1. 第一步声明一个单例类singletonCar
  2. 第二步声明一个指向单例对象的指针,但是不进行实例化
  3. 第三步对外提供一个全局访问函数GetSingleton来获取这个单例对象
  4. 第四步判断这个单例是否已经实例化,未实例化则进行实例化操作
代码问题:

上述懒汉模式代码如果是在并发场景下的话,就会存在问题,可能会有多个goroutine在同一时刻调用GetSingleton()方法获取单例对象。那么就会创建两个单例对象,其中一个单例对象会被浪费,成为内存垃圾。这就是懒汉模式所存在的并发安全问题

改进一:

懒汉模式v2.drawio.png

那么既然存在并发安全问题,我们最先想到的解决方法就是加锁,所以就有了第二个版本的懒汉模式的代码(只体现改动部分)

// 新增锁
var lock sync.Mutex// 懒汉模式在获取对象的时候才会实例化对象
func GetSingleton() SingletonCarInterface {// 获取对象前,先加锁lock.Lock()defer lock.Unlock()// 不存在对象,则实例化对象if s == nil {s = newSingletonCar()}return s
}
代码解释:

获取单例对象进行加锁操作,可以保证同一时刻只有一个goroutine获取到互斥锁,从而保证只有第一个进入的goroutine能够创建这个唯一的单例对象,后面的goroutine可以获取到这个唯一的单例对象。

代码问题:

这样写虽然可以解决并发安全的问题,但是由于加锁操作,对性能影响是比较大的,所以这不是一个高效的写法。

改进二:

懒汉模式v3.drawio.png

针对于加锁性能低下的问题,我们可以使用原子读操作来解决问题,即并不让每一个goroutine都对GetSingleton()方法获取锁,而是首先进行一个原子读操作,只有这个原子值不条件,才允许这个goroutine获取锁。这样可以大大提升性能,我们来看一下代码:

// 新增锁
var lock sync.Mutex// 原子读操作标记位
var syncNum uint32// 懒汉模式在获取对象的时候才会实例化对象
func GetSingleton() SingletonCarInterface {if atomic.LoadUint32(&syncNum) == 1 {return s}// 获取对象前,先加锁lock.Lock()defer lock.Unlock()// 不存在对象,则实例化对象if s == nil {s = newSingletonCar()// 对syncNum这个标记位进行复制操作atomic.StoreUint32(&syncNum, 1)}return s
}
代码解释:

首先进行原子读操作,当标记位是0时,说明没有实例化过这个对象,然后进行加锁操作,实例化单例对象。

tips:atomic.LoadUint32 是 Go 语言中 sync/atomic 包提供的一个函数,用于原子性地加载一个 uint32 类型的值。这个函数的目的是在多线程或并发的情况下,确保对该变量的读取操作是原子的,不会被中断或被其他线程的写操作影响,避免竞态条件和数据竞争的问题。

饿汉模式和懒汉模式对比:

  • 饿汉模式:程序运行时,即刻创建,无论之后是否被用到,也无论性能损耗如何,说起来不够智能
  • 懒汉模式:虽然看起来比较智能,但是如果初始化方法有问题,可能会出现安全隐患

golang内置方法

golang自带sync.Once()方法,该方法能够保证内部的函数只执行一次。我们可以使用该方法来创建一个单例对象,代码如下:

package mainimport ("fmt""sync"
)// 1.单例类需要是包内私有的,不能被外界访问到,否则就能实例化多个对象
type singletonCar struct {name string
}type SingletonCarInterface interface {PrintName()
}// 2.访问单例对象的指针必须是私有指针,不能被外界访问到,否则外界就能修改这个指针的指向,导致单例对象丢失
var s *singletonCarfunc newSingletonCar() *singletonCar {return &singletonCar{name: "BMW",}
}func (sc *singletonCar) PrintName() {fmt.Println(sc.name)
}var once sync.Once// 3.使用sync.Once来保证只实例化一次
func GetSingleton() SingletonCarInterface {once.Do(func() {s = newSingletonCar()})return s
}func main() {sc := GetSingleton()sc.PrintName()
}

可以看到,once.Do的源码内部也是使用了原子读操作来创建的单例

func (o *Once) Do(f func()) {// Note: Here is an incorrect implementation of Do://// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {//    f()// }//// Do guarantees that when it returns, f has finished.// This implementation would not implement that guarantee:// given two simultaneous calls, the winner of the cas would// call f, and the second would return immediately, without// waiting for the first's call to f to complete.// This is why the slow path falls back to a mutex, and why// the atomic.StoreUint32 must be delayed until after f returns.if atomic.LoadUint32(&o.done) == 0 {// Outlined slow-path to allow inlining of the fast-path.o.doSlow(f)}
}

总结:

本文介绍了什么是单例模式(一个类全局只能存在一个实例对象,并且对外只提供一个访问点);用途有哪些场景(访问全局资源;初始化操作很耗时;作为模块或者函数入参非常复杂时);按照对象创建时机不同,分为饿汉模式和懒汉模式两种(饿汉模式:程序启动时创建,懒汉模式:需要用到时创建)以及饿汉模式和懒汉模式的各种使用情况以及有哪些问题。

写在最后:

感谢大家的阅读,晴天将继续努力,分享更多有趣且实用的主题,如有错误和纰漏,欢迎给予指正。 更多文章敬请关注作者个人公众号 晴天码字。 我们下期不见不散,to be continued…

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

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

相关文章

微信小程序隐私政策不合规,应当由用户自主阅读后自行选择是否同意隐私政策协议,不得默认强制用户同意

小程序隐私政策不合规,默认自动同意《用户服务协议》及《隐私政策》,应当由用户自主阅读后自行选择是否同意隐私政策协议,不得默认强制用户同意,请整改后再重新提交。 把 登录代表同意《用户协议》和《隐私政策》 改为 同意《用…

使用迁移学习在线校准深度学习模型

使用迁移学习在线校准深度学习模型 本文参考的是2023年发表于Engineering Applications of Artificial Intelligence, EAAI的Deep Gaussian mixture adaptive network for robust soft sensor modeling with a closed-loop calibration mechanism 1. 动机 概念漂移导致历史训…

基于SSM的旅游管理系统的设计与实现

末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:采用JSP技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目&#x…

Python:词法分析(行结构与显式、隐式行拼接)

相关阅读 Pythonhttps://blog.csdn.net/weixin_45791458/category_12403403.html?spm1001.2014.3001.5482 1、逻辑结构 一个Python程序由许多逻辑行组成,字面意义上的一行指的是末尾有换行符(\n),但在不同的情况下,行末尾的换行符(\n)可能有…

数据分析实战 | K-means算法——蛋白质消费特征分析

目录 一、数据及分析对象 二、目的及分析任务 三、方法及工具 四、数据读入 五、数据理解 六、数据准备 七、模型训练 ​编辑 八、模型评价 九、模型调参与预测 一、数据及分析对象 txt文件——“protein.txt”,主要记录了25个国家的9个属性,主…

【python海洋专题四十四】海洋指数画法--多色渐变柱状图

【python海洋专题四十四】海洋指数画法–多色渐变柱状图

登录注册代码模板(Vue3+SpringBoot)[邮箱发送验证码(HTML)、RSA 加密解密(支持长文本)、黑暗与亮色主题切换、AOP信息校验]

文章归档:https://www.yuque.com/u27599042/coding_star/cx5ptule64utcr9e 仓库地址 https://gitee.com/tongchaowei/login-register-template 网页效果展示 相关说明 在该代码模板中,实现了如下功能: 邮箱发送验证码(邮件内容…

C#动态拦截并覆盖第三方进程的函数,实现函数篡改(外挂)

今天在看之前收藏的一个pdf文档(介绍C#外挂的相关知识的),结合网上的东西及个人的理解才有了这篇文章。 参考文章: 【精选】一文带解读C# 动态拦截覆盖第三方进程中的函数(外挂必备)_zls365365的博客-CSDN博客 DotNetDetour - …

知识蒸馏概述及开源项目推荐

文章目录 1.介绍2.知识2.1 基于响应的知识(response-based)2.2 基于特征的知识(feature-based)2.3 基于关系的知识(relation-based) 3.蒸馏机制3.1 离线蒸馏3.2 在线蒸馏3.3 自蒸馏 4.教师-学生架构5.蒸馏算法5.1 对抗性蒸馏(Adversarial Dis…

vim相关命令讲解!

本文旨在讲解vim 以及其相关的操作! 希望读完本文,读者会有一定的收获!好的,干货马上就来! 初识vim 在讲解vim之前,我们首先要了解vim是什么,有什么作用?只有了解了vim才能更好的理…

【CASS精品教程】cass3d 11.0加载超大影像、三维模型、点云数据

CAD2016+CASS11.0(内置3d)下载与安装: 【CASS精品教程】CAD2016+CASS11.0安装教程(附CASS11.0安装包下载)https://geostorm.blog.csdn.net/article/details/132392530 一、cass11.0 3d支持的数据 cass11.0中的3d模块增加了多种数据的支持,主要有: 1. 三维模型 点击…

ROC 曲线详解

前言 ROC 曲线是一种坐标图式的分析工具,是由二战中的电子和雷达工程师发明的,发明之初是用来侦测敌军飞机、船舰,后来被应用于医学、生物学、犯罪心理学。 如今,ROC 曲线已经被广泛应用于机器学习领域的模型评估,说…