go语言实现设计模式—单例模式

1 背景

在研读业务代码时,我发现自定义类型会以“懒汉式-非线程安全”和“饿汉式”进行实例化,因此对单例模式产生兴趣,它是什么?原理是怎样的?解决了什么样的问题?应用场景如何?优缺点是什么?

2 什么是单例

单例设计模式(Singleton Design Pattern): 一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

单例模式是最简单的设计模式之一,它提供了创建对象的最佳方式。

这种模式涉及到一个单一的结构体,该结构体负责创建自己的对象,同时确保只有单个对象被创建。即多次创建一个结构体的对象,得到的对象的存储地址永远与第一次创建对象的存储地址相同。

3 为什么使用单例

3.1 资源访问冲突问题

goroutine1:
{
 file1 = new(File)
 file1.write("abc")
}
goroutine2:
{
 file2 = new(File)
 file2.write("defh")
}

多协程对同一文件的实例进行写操作时,就有可能存在写入的数据互相覆盖的情况。
在这里插入图片描述

3.2 解决方式

  • 1 加锁,同一个文件的所有实例共享一把锁
  • 2 分布式锁,但实现安全可靠、无 bug、高性能较难
  • 3 并发队列,多个线程同时往并发队列写数据,一个单独线程将并发队列中的数据写入文件
  • 4 单例模式 :
    • 不用创建那么多实例,节省内存空间,
    • 节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源)

3.3 应用场景

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类

1.需要频繁实例化然后销毁的对象。 
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 
3.有状态的工具类对象。 
4.频繁访问数据库或文件的对象。 
  • 配置信息类:在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。
  • 日志应用:共享的日志文件一直处于打开状态,因为只能有一个实例去操作。
  • 数据库连接池:用单例模式来维护频繁打开或者关闭数据库连接所引起的效率损耗
  • 多线程的线程池:采用单例模式对池中的线程进行控制。
  • 操作系统的文件系统:一个操作系统只能有一个文件系统,也是单例模式实现的例子

3.4 设计思考

  • 考虑对象创建时的线程安全问题
  • 考虑是否支持延迟加载
  • 考虑 getInstance() 性能是否高(是否加锁)

4 如何创建单例

单例模式是最简单的设计模式之一,它提供了创建对象的最佳方式。
这种模式涉及到一个单一的结构体,该结构体负责创建自己的对象,同时确保只有单个对象被创建。即多次创建一个结构体的对象,得到的对象的存储地址永远与第一次创建对象的存储地址相同。

4.1 饿汉式-线程安全

直接创建对象,线程安全。
缺点

  • 在导入包/init的同时会创建该对象,并持续占有在内存中。
  • 这样的实现方式不支持延迟加载(在真正用到再创建实例)
package singleton

type School struct{}

var (
	instance *School
)

func init(){
  instance = new(School)
}

func GetInstance() *School {
	return instance
}

4.2 懒汉式-非线程安全

优点是支持延迟加载
缺点是通过懒汉式-非线程安全方式创建单例,在并发下可能会多次创建

package singleton

type School struct{}

var (
	instance *School
)

func GetInstance() *School {
	if instance == nil {
		instance = new(School)
	}
	return instance
}

4.3 懒汉式-线程安全

在非线程安全的基础上,利用Sync.Mutex进行加锁,保证线程安全,
但由于每次调用该方法都进行了加锁操作,在性能上相对不高效

package singleton

type School struct {}

var (
	instance *School
	lock     *sync.Mutex = &sync.Mutex{}
)

func GetInstance() *School {
	lock.Lock()
	defer lock.Unlock()
	if instance == nil {
		instance = new(School)
	}
	return instance
}

4.4 双重检查

在懒汉式(线程安全)的基础上再进行忧化,通过判断来减少加锁的操作。保证线程安全同时不影响性能。这种实现方式解决了懒汉式并发度低的问题。
设计思路:第一次判断不加锁,第二次加锁保证线程安全,一旦对象建立后,获取对象就不用加锁了

package singleton

var (
	instance *School
	lock     sync.Mutex
)

func GetInstance() *School {
	if instance == nil {
		lock.Lock() //第一次判断不加锁,第二次加锁保证线程安全,一旦对象建立后,获取对象就不用加锁了
		if instance == nil {
			instance = new(School)
		}
		lock.Unlock()
	}
	return instance
}

4.5 once写法:推荐采用

利用 sync.Once 方法Do,确保Do中的方法只被执行一次的特性,创建单个结构体实例。
使用Do方法也巧妙的保证了并发线程安全。

package singleton

var (
	instance *School
	once     sync.Once
)

func GetInstance() *School {
	once.Do(func() {
		instance = new(School)
	})
	return instance
}

其中Once源码利用atomic操作实现f函数只执行一次

  • 进行加锁,再做一次判断,如果没有执行,则进行标志已经执行并调用该方法
  • 判断是否执行过该方法,如果执行过则不执行
func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 1 {  判断是否执行过该方法,如果执行过则不执行
	   return
	}
	// Slow-path.
	o.m.Lock()
	defer o.m.Unlock()   进行加锁,再做一次判断,如果没有执行,则进行标志已经执行并调用该方法
	if o.done == 0 {
	   defer atomic.StoreUint32(&o.done, 1)
	   f()
	}
 }

5 采用Once实现单例模式

package main

import (
	"fmt"
	"strconv"
	"sync"
)

type School struct {
	name string
	id   int
}

var (
	instance *School
	once     sync.Once
	wg       sync.WaitGroup
)

func GetInstance(name string, id int) *School {
	once.Do(func() {
		fmt.Println("------------init-----------")
		instance = new(School)
	})
	instance.name = name
	instance.id = id
	return instance
}

func main() {

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(seq int) {
			defer wg.Done()
			instance = GetInstance("xiaobai"+strconv.Itoa(seq), 25)
			fmt.Printf("gonum: %s, address: %p, name: %s, id: %d\n",
				strconv.Itoa(seq), instance, instance.name, instance.id)
		}(i)
	}
	
	wg.Wait()
	fmt.Println("end!")
}

运行结果
在这里插入图片描述

6 其他

6.1 借鉴

本文go设计模式实现单例模式,借鉴于Java设计模式,存在未考虑完善的地方:

  • Java中单例模式会存在指令重排序问题
  • 懒汉式还是饿汉式更好需要看具体的场景。对于那些短生命周期的应用,如客户端应用来说,启动是频繁发生的,如果启动时导致了一堆饿汉初始化,会给用户带来不好的体验,如果把初始化往后延,将初始化分散在未来的各个时间点,即使某个懒汉初始化时间较长,用户也几乎无感知。而对于生命周期较长的应用,长痛不如短痛,启动时耗点时,保证后面的使用流畅也是可取的。
  • 对于单例存在的其他问题,比如对 OOP 特性、扩展性、可测性不友好等问题,还是无法解决。所以,如果要完全解决这些问题,可能要从根上,寻找其他方式来实现全局唯一。实际上,实例的全局唯一性可以通过多种不同的方式来保证。我们既可以通过单例模式来强制保证,也可以通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,还可以通过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。
  • 如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,与其在其他地方 new ,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。

6.2 思考

  • 如何理解单例模式中的唯一性?
  • 如何实现线程唯一的单例?
  • 如何实现集群环境下的单例?
  • 如何实现一个多例模式?

参考资料

1 https://time.geekbang.org/column/article/194035
2 https://blog.csdn.net/TCatTime/article/details/106882600
3 https://blog.csdn.net/qq_37703616/article/details/81989889
4 https://github.com/tmrts/go-patterns/blob/master/creational/singleton.md

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值