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

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

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

什么是单例模式

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

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

单例模式类图.png

为什么需要单例模式

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

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

单例模式的分类

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

饿汉模式

单例创建步骤.drawio.png

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

package main

import "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() // BMW
    singleCar2 := GetSingleCar()
    singleCar2.PrintCarName() // BMW
    fmt.Println(singleCar == singleCar2) //true
}
代码解析:
  1. 第一步声明一个单例类singletonCar
  2. 第二步声明一个指向单例对象的指针,并初始化单例对象
  3. 第三步对外提供一个全局访问函数GetSingleton来获取这个单例对象
问题讨论:

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

改进:

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

type SingletonCarInterface interface {
    PrintCarName()
}

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

懒汉模式

懒汉模式.drawio.png

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

package main

import "fmt"

// 1.单例类需要是包内私有的,不能被外界访问到,否则就能实例化多个对象
type singletonCar struct {
    name string
}

type SingletonCarInterface interface {
    PrintName()
}

// 2.访问单例对象的指针必须是私有指针,不能被外界访问到,否则外界就能修改这个指针的指向,导致单例对象丢失
var s *singletonCar

func 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 main

import (
    "fmt"
    "sync"
)

// 1.单例类需要是包内私有的,不能被外界访问到,否则就能实例化多个对象
type singletonCar struct {
    name string
}

type SingletonCarInterface interface {
    PrintName()
}

// 2.访问单例对象的指针必须是私有指针,不能被外界访问到,否则外界就能修改这个指针的指向,导致单例对象丢失
var s *singletonCar

func 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…

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】WSL安装Kali及基本操作

😏★,:.☆( ̄▽ ̄)/$:.★ 😏 这篇文章主要介绍WSL安装Kali及基本操作。 学其所用,用其所学。——梁启超 欢迎来到我的博客,一起学习,共同进步。 喜欢的朋友可以关注一下,下次更新不迷路…

<C++> stack queue模拟实现

目录 前言 一、stack的使用 1. 接口说明 2. 例题 二、模拟实现stack 三、queue的使用 四、模拟实现queue 五、deque 总结 前言 LIFO stack 1. 栈是一种容器适配器,专门设计用于在后进先出上下文(后进先出)中运行,其中元素仅从容器…

Linux之基本指令操作

1、whoami whoami:查看当前账号是谁 2、who who:查看当前我的系统当中有哪些用户,当前有哪些人登录了我的机器 3、 pwd pwd:查看我当前所处的目录,就好比Windows下的路径 4、ls ls:查看当前目录下的文件信…

算法导论6:摊还分析,显式与隐式

P258 摊还分析概念 聚合分析,利用它,我们证明对于n,一个n个操作的序列最坏情况下的花费的总时间为T(n),因此,在最坏情况下,每个操作的平均代价(摊还代价)为T(n)/n 举了例子来形容这…

头歌答案Python——JSON基础

目录 ​编辑 Python——JSON基础 第1关:JSON篇:JSON基础知识 任务描述 第2关:JSON篇:使用json库 任务描述 Python——XPath基础 第1关:XPath 路径表达式 任务描述 第2关:XPath 轴定位 任务描述…

SOME/IP 协议介绍(四)RPC协议规范

RPC协议规范 本章描述了SOME/IP的RPC协议。 传输协议绑定 为了传输不同传输协议的SOME/IP消息,可以使用多种传输协议。SOME/IP目前支持UDP和TCP。它们的绑定在以下章节中进行了解释,而第[SIP_RPC_450页,第36页]节讨论了选择哪种传输协议。…

[C国演义] 第十八章

第十八章 最长斐波那契子序列的长度最长等差数列等差序列划分II - 子序列 最长斐波那契子序列的长度 力扣链接 子序列 ⇒ dp[i] — — 以 arr[i] 结尾的所有子序列中, 斐波那契子序列的最长长度子序列 ⇒ 状态转移方程 — — 根据最后一个位置的组成来划分 初始化 — — 根…

海外媒体发稿:彭博社发稿宣传中,5种精准营销方式

在如今的信息发生爆炸时期,营销方式多种多样,但是充分体现精准营销并针对不同用户群体的需求并非易事。下面我们就根据彭博社发稿营销推广为例子,给大家介绍怎样根据不同用户人群方案策划5种精准营销方式。 1.界定总体目标用户人群在制订精准…

Flink SQL 表值聚合函数(Table Aggregate Function)详解

使用场景: 表值聚合函数即 UDTAF,这个函数⽬前只能在 Table API 中使⽤,不能在 SQL API 中使⽤。 函数功能: 在 SQL 表达式中,如果想对数据先分组再进⾏聚合取值: select max(xxx) from source_table gr…

华为ensp搭建小型园区网络规划

文章目录 前言一、拓扑图二、数据规划三、设备配置四.配置命令1.配置接入层交换机ACC11.1 设备命名,创建VLAN1.2 配置eth-trunk 11.3 配置用户端 2.配置核心层交换机CORE2.1设备命名2.2配置Eth-Trunk2.3 vlan配置ip2.4 上行接口配置 3.DHCP配置3.1 CORE: 4.配置路由…

计算机毕业设计:疲劳驾驶检测识别系统 python深度学习 YOLOv5 (包含文档+源码+部署教程)

[毕业设计]2023-2024年最新最全计算机专业毕设选题推荐汇总 1、项目介绍 基于YOLOv5的疲劳驾驶检测系统使用深度学习技术检测常见驾驶图片、视频和实时视频中的疲劳行为,识别其闭眼、打哈欠等结果并记录和保存,以防止交通事故发生。本文详细介绍疲劳驾…

ROC 曲线详解

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

模板初阶 C++

目录 泛型编程 函数模板 概念 格式 原理 函数模板的实例化 类模板 格式 类模板的实例化 泛型编程 当我们要实现一个交换函数,我们可以利用函数重载实现,但是有几个不好的地方 1.函数重载仅仅是类型不同,代码复用率较低,只…

pyorch Hub 系列#4:PGAN — GAN 模型

一、主题描述 2014 年生成对抗网络的诞生及其对任意数据分布进行有效建模的能力席卷了计算机视觉界。两人范例的简单性和推理时令人惊讶的快速样本生成是使 GAN 成为现实世界中实际应用的理想选择的两个主要因素。 然而,在它们出现后的很长一段时间内,GA…

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

文章目录 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…

Linux基础开发工具之调试器gdb

文章目录 1.编译成的可调试的debug版本1.1gcc test.c -o testdebug -g1.2readelf -S testdebug | grep -i debug 2.调试指令2.0quit退出2.1list/l/l 数字: 显示代码2.2run/r运行2.3断点相关1. break num/b num: 设置2. info b: 查看3. d index: 删除4. n: F10逐过程5. p 变量名…

Python文件、文件夹操作汇总

目录 一、概览 二、文件操作 2.1 文件的打开、关闭 2.2 文件级操作 2.3 文件内容的操作 三、文件夹操作 四、常用技巧 五、常见使用场景 5.1 查找指定类型文件 5.2 查找指定名称的文件 5.3 查找指定名称的文件夹 5.4 指定路径查找包含指定内容的文件 一、概览 ​在…

CSS注入的四种实现方式

目录 CSS注入窃取标签属性数据 简单的一个实验: 解决hidden 方法1:jsnode.js实现 侧信道攻击 方法2:对比波兰研究院的方案 使用兄弟选择器 方法3:jswebsocket实现CSS注入 实验实现: 方法4:window…

【云备份|| 日志 day6】文件业务处理模块

云备份day6 业务处理 业务处理 云备份项目中 ,业务处理模块是针对客户端的业务请求进行处理,并最终给与响应。而整个过程中包含以下要实现的功能: 借助网络通信模块httplib库搭建http服务器与客户端进行网络通信针对收到的请求进行对应的业…

算法导论笔记4:散列数 hash

一 了解一些散列的基本概念,仅从文字角度,整理了最基础的定义。 发现一本书,《算法图解》,微信读书APP可读,有图,并且是科普性质的读物,用的比喻很生活化,可以与《算法导论》合并起…