Singleton 单例模式

Singleton 单例模式


Design Patten
Go Design Patten

什么是单例模式

单例模式是指声明一个类并保证这个类只存在全局唯一的实例供外部使用

简单来讲,单例模式就是在全局视野下外部使用单个实例

应用场景

  • 只允许存在一个实例的类,如作用于全局的统计或唯一标识生成器等
  • 实例化时严重耗费系统资源的类,如池化(连接池、协程池)、与第三方进行交互的客户端
  • 入参复杂的系统模块组件,如 MVC 架构下的 controller、service、dao 等

实现模式

饿汉式

应用启动即完成单例的初始化

实现流程

Hungry

  1. 单例类和构造方法声明为不可导出类型,避免外部直接获取(避免外界直接初始化类实例)
  2. 在程序启动时,初始化好一个全局单一的实例
  3. 暴露一个可导出的获取实例的方法,用于返回当前单例对象
// 饿汉式单例模式实现
package singleton
// 定义导出方法
type Singleton interface{
Work()
}
// 定义实现类
type singleton struct{}
// 声明单例
var s *singleton
// 实现单例方法
func (s *singleton) Work() {}
// 初始化单例
func init() {
s = newSingleton()
}
// 构造方法
func newSingleton() *singleton {
return &singleton{}
}
// 导出获取单例方法
func GetSingleton() Singleton {
return s
}

懒汉式

被使用到时再执行初始化

实现流程

Lazybones

  1. 单例类和构造方法声明为不可导出类型,避免外部直接获取(避免外界直接初始化类实例)
  2. 声明一个全局单一的实例,但不进行初始化
  3. 暴露一个可导出的获取实例的方法,用于返回当前单例对象
  4. 在获取实例的方法被调用时,判断单例是否初始化,选择是否初始化与返回单例
// 懒汉式单例模式实现
package singleton
import "sync"
// 声明单例与互斥锁
var (
s *singleton
once sync.Once
)
// 定义导出方法
type Singleton interface {
Work()
}
// 定义实现类
type singleton struct{}
// 实现方法
func (s *singleton) Work() {}
// 构造函数
func newSingleton() *singleton {
return &singleton{}
}
// 导出获取单例方法
func GetSingleton() Singleton {
once.Do(func() {
s = newSingleton()
})
return s
}
  • 饿汉式单例模式的演进之路

    // 懒汉式单例模式实现
    package singleton
    var s *singleton
    type Singleton interface{
    Work()
    }
    type singleton struct{}
    func (s *singleton) Work() {}
    func newSingleton() *singleton {
    return &singleton{}
    }
    func GetSingleton() Singleton {
    if s == nil {
    s = newSingleton()
    }
    return s
    }

    如果系统中存在并发调用,那 singleton 便有可能被初始化多次,违背了全剧唯一的原则,因此我们再次基础上进行加锁,得到了以下代码

    // 懒汉式单例模式实现
    package singleton
    var (
    s *singleton
    mux sync.Mutex
    )
    type Singleton interface{
    Work()
    }
    type singleton struct{}
    func (s *singleton) Work() {}
    func newSingleton() *singleton {
    return &singleton{}
    }
    func GetSingleton() Singleton {
    mux.Lock()
    defer mux.Unlock()
    if s == nil {
    s = newSingleton()
    }
    return s
    }

    那我们这样实现总没有问题了吧?其实是有的,这样实现确实是避免了并发导致初始化多个实例的情况,但是在每次获取单例时,都会进行加锁操作,会造成无意义的性能浪费

    // 懒汉式单例模式实现
    package singleton
    var (
    s *singleton
    mux sync.Mutex
    )
    type Singleton interface{
    Work()
    }
    type singleton struct{}
    func (s *singleton) Work() {}
    func newSingleton() *singleton {
    return &singleton{}
    }
    func GetSingleton() Singleton {
    if s != nil {
    return s
    }
    mux.Lock()
    defer mux.Unlock()
    s = newSingleton()
    return s
    }

    那这样我们便解决了每次获取单例时都需要加锁的情况了,但是当前代码好像又引入了新的并发安全问题

    • 当没有实例化对象时,并发获取单例会引起锁竞争,当获取到锁的协程初始化单例后,释放锁,处于阻塞等待状态的另一个协程获取到锁后,会重新进行初始化,造成多个实例

    因此我们引入了 double check 的机制来解决这个问题,代码如下

    // 懒汉式单例模式实现
    package singleton
    import "sync"
    // 声明单例与互斥锁
    var (
    s *singleton
    mux sync.Mutex
    )
    // 定义导出方法
    type Singleton interface {
    Work()
    }
    // 定义实现类
    type singleton struct{}
    // 实现方法
    func (s *singleton) Work() {}
    // 构造函数
    func newSingleton() *singleton {
    return &singleton{}
    }
    // 导出获取单例方法
    func GetSingleton() Singleton {
    // 如果单例完成初始化,返回单例
    if s != nil {
    return s
    }
    // 加锁保证只有一个初始化单例任务在执行
    mux.Lock()
    defer mux.Unlock
    // double check
    // 防止等待线程获取锁后再次初始化单例
    if s != nil {
    return s
    }
    // 初始化单例
    s = newSingleton()
    // 返回单例
    return s
    }

Singleton

  • 而 double check 的实现思路就已经非常接近 sync.Once 的实现思路了

    // Do calls the function f if and only if Do is being called for the
    // first time for this instance of Once. In other words, given
    //
    // var once Once
    //
    // if once.Do(f) is called multiple times, only the first call will invoke f,
    // even if f has a different value in each invocation. A new instance of
    // Once is required for each function to execute.
    //
    // Do is intended for initialization that must be run exactly once. Since f
    // is niladic, it may be necessary to use a function literal to capture the
    // arguments to a function to be invoked by Do:
    //
    // config.once.Do(func() { config.init(filename) })
    //
    // Because no call to Do returns until the one call to f returns, if f causes
    // Do to be called, it will deadlock.
    //
    // If f panics, Do considers it to have returned; future calls of Do return
    // without calling f.
    func (o *Once) Do(f func()) {
    // Note: Here is an incorrect implementation of Do:
    //
    // if o.done.CompareAndSwap(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 o.done.Store must be delayed until after f returns.
    if o.done.Load() == 0 {
    // Outlined slow-path to allow inlining of the fast-path.
    o.doSlow(f)
    }
    }
    func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done.Load() == 0 {
    defer o.done.Store(1)
    f()
    }
    }

    Dubbo Check

    • sync.once 也是基于 double check 机制实现对全局任务单次执行的保证
      1. 检查 Once.Done 的值是否为 0,由于此处的 Done 采用的是 atomic.uint32 , 因此在这里避免了繁重的加锁动作
      2. 如果不为 0 则证明该任务已被执行
      3. 如果为 0,则进行加锁操作
      4. Once.Done 的值进行第二次检测,确保在竞争锁期间 Once.Done 的值为发生改变,即该任务未执行
      5. 执行任务并解锁

对比

  • 饿汉式在程序启动时便会对单例进行初始化,如果单例对象迟迟不被使用,甚至永远不被使用,那初始化过程可能会是一次无用的性能损耗
  • 懒汉式在单例首次被使用时才会被初始化,但如果初始化工作中存在异常,则会导致程序崩溃,因此,如果可能导致程序奔溃或者存在异常的单例(e.g.数据库连接池),应该在代码编译运行之初就提前暴露,因此更适合采用饿汉式单例模式,从而更有利于问题定位与解决