最近打算集中一段时间复习一下设计模式,今天复习的第一个就是单例模式

当我们聊到单例模式的时候,我们可以知道它一般有两种实现方式,即懒汉模式饿汉模式

懒汉模式

import "sync"

type Singleton struct{}

var (
	singleton *Singleton
  once = &sync.Once{}
)

func Get() *Singleton {
  if singleton == nil {
    once.Do(func(){
      singleton = &Singleton{}
    })
  }
  return singleton
}

饿汉模式

type Singleton struct{}

var singleton *Singleton

func	init() {
  singleton = &Singleton
}

func Get() *Singleton {
  return singleton
}

饿汉模式懒汉模式的区别在于:

  • init函数是在package首次被加载时执行,若singleton一直没有被使用,则浪费了内存,也延长了程序的加载时长
  • sync.Once是在使用时再执行,在并发的场景下也是线程安全的

顺带,我们来看一下sync.Once的底层实现:

package sync

import (
	"sync/atomic"
)

type Once struct {
  done uint32
  m Mutex
}

func (o *Once) Do(f func()) {
  if atomic.LoadUint32(&o.done) == 0 {
    o.doSlow(f)
  }
}

func (o *Once) doSlow(f func()) {
  o.m.Lock()
  defer o.m.Unlock()
  
  if o.done == 0 {
    defer atomic.StoreUint32(&o.done, 1)
    f()
  }
}

在调用Do()的时候,加载是否被执行done,如果没有则调用doSlow(),在加锁后重新判断done的值是否有变化。那么为什么会判断两次呢?原因是:

  1. 如果有多个goroutine调用这个代码,其中一个获取到了锁,另外的就不会去调用f()。但是f()是否执行完是未知的
  2. 所以为了保证先执行f(),再设置标志位done,f()一定被执行,需要对done做原子操作