单例模式也是属于创建型模式
实际开发中很多对象我们都是临时需要的时候创建一个就行了,比如一个goods对象,比如一个user对象等,但是有些对象全局只能或者只需要创建一次,后面该方法即使再次被调用到也不能反复实例化
比如数据库连接池, 比如log的配置等等, 没有必要打开多次,或者打开多次容易出错。一次因为耗时,而是反复建立连接服务器会耗完连接或者拒绝连接。

我们能想到的最简单的单例方式 - 错误示范

  1. package singleton
  2. type DBPool struct {}
  3. var dbpoolIns *DBPool
  4. func GetDBPool() *DBPool {
  5. if dbpoolIns == nil {
  6. dbpoolIns = &DBPool{} // 不是并发安全的, 此处省略具体的创建过程
  7. }
  8. return dbpoolIns
  9. }

在上述情况下,多个goroutine可以执行第一个检查,并且它们都将创建该dbpoolIns类型的实例并相互覆盖。无法保证它将在此处返回哪个实例,并且对该实例的其他进一步操作可能与开发人员的期望不一致。
不好的原因是,如果有代码保留了对该单例实例的引用,则可能存在具有不同状态的该类型的多个实例,从而产生潜在的不同代码行为。这也成为调试过程中的一个噩梦,并且很难发现该错误,因为在调试时,由于运行时暂停而没有出现任何错误,这使非并发安全执行的可能性降到了最低,并且很容易隐藏开发人员的问题。

通过加锁解决并发问题

也有很多对这种并发安全问题的糟糕解决方案。使用下面的代码确实能解决并发安全问题,但会带来其他潜在的严重问题,通过加锁把对该函数的并发调用变成了串行。

  1. var mu Sync.Mutex
  2. func GetDBPool() *DBPool {
  3. mu.Lock() // 如果实例存在没有必要加锁
  4. defer mu.Unlock()
  5. if dbpoolIns == nil {
  6. dbpoolIns = &DBPool{}
  7. }
  8. return dbpoolIns
  9. }

在上面的代码中,我们可以看到在创建单例实例之前通过引入Sync.Mutex和获取Lock来解决并发安全问题。问题是我们在这里执行了过多的锁定,即使我们不需要这样做,在实例已经创建的情况下,我们应该简单地返回缓存的单例实例。在高度并发的代码基础上,这可能会产生瓶颈,因为一次只有一个goroutine可以获得单例实例。
因此,这不是最佳方法。我们必须考虑其他解决方案。
注意:如果该代码视频不频繁的资源上无所谓但是高并发调用就会导致性能下降。

改进一下提高并发

Check-Lock-Check模式

在C ++和其他语言中,确保最小程度的锁定并且仍然是并发安全的最佳和最安全的方法是在获取锁定时利用众所周知的Check-Lock-Check模式。该模式的伪代码表示如下。

  1. if check() {
  2. lock() {
  3. if check() {
  4. // 在这里执行加锁安全的代码
  5. }
  6. }
  7. }

该模式背后的思想是,你应该首先进行检查,以最小化任何主动锁定,因为IF语句的开销要比加锁小。其次,我们希望等待并获取互斥锁,这样在同一时刻在那个块中只有一个执行。但是,在第一次检查和获取互斥锁之间,可能有其他goroutine获取了锁,因此,我们需要在锁的内部再次进行检查,以避免用另一个实例覆盖了实例。
如果将这种模式应用于我们的GetInstance()方法,我们会写出类似下面的代码:

  1. func GetDBPool() *DBPool {
  2. if dbpoolIns == nil { // 不太完美 因为这里不是完全原子的
  3. mu.Lock()
  4. defer mu.Unlock()
  5. if dbpoolIns == nil {
  6. dbpoolIns = &DBPool{}
  7. }
  8. }
  9. return dbpoolIns
  10. }

使用atomic提高并发

  1. import "sync"
  2. import "sync/atomic"
  3. var initialized uint32
  4. func GetDBPool() *DBPool {
  5. if atomic.LoadUInt32(&initialized) == 1 { // 原子操作
  6. return dbpoolIns
  7. }
  8. mu.Lock()
  9. defer mu.Unlock()
  10. if initialized == 0 {
  11. dbpoolIns = &DBPool{}
  12. atomic.StoreUint32(&initialized, 1)
  13. }
  14. return dbpoolIns
  15. }

但是……这看起来有点繁琐了,我们其实可以通过研究Go语言和标准库如何实现goroutine同步来做得更好。

使用内置的sync.Once

们希望利用Go惯用的方式来实现这个单例模式。我们在标准库sync中找到了Once类型。它能保证某个操作仅且只执行一次。下面是来自Go标准库的源码(部分注释有删改)。

  1. // Once is an object that will perform exactly one action.
  2. type Once struct {
  3. // done indicates whether the action has been performed.
  4. // It is first in the struct because it is used in the hot path.
  5. // The hot path is inlined at every call site.
  6. // Placing done first allows more compact instructions on some architectures (amd64/x86),
  7. // and fewer instructions (to calculate offset) on other architectures.
  8. done uint32
  9. m Mutex
  10. }
  11. func (o *Once) Do(f func()) {
  12. if atomic.LoadUint32(&o.done) == 0 { // check
  13. // Outlined slow-path to allow inlining of the fast-path.
  14. o.doSlow(f)
  15. }
  16. }
  17. func (o *Once) doSlow(f func()) {
  18. o.m.Lock() // lock
  19. defer o.m.Unlock()
  20. if o.done == 0 { // check
  21. defer atomic.StoreUint32(&o.done, 1)
  22. f()
  23. }
  24. }

借助sync.once重构

  1. package singleton
  2. import (
  3. "sync"
  4. )
  5. type DBPool struct {}
  6. var dbpoolIns *singleton
  7. var once sync.Once
  8. func GetDBPool() *DBPool {
  9. once.Do(func() {
  10. dbpoolIns = &DBPool{}
  11. })
  12. return dbpoolIns
  13. }

因此,使用sync.Once包是安全地实现此目标的首选方式,类似于Objective-C和Swift(Cocoa)实现dispatch_once方法来执行类似的初始化。