目录

golang 单例和once详解

饿汉式

type singleton struct {

}

var instance = new(singleton)

func GetInstance()  *singleton{
    return instance
}

或者
type singleton struct {

}

var instance *singleton

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

func GetInstance()  *singleton{
    return instance
}

这两种方法都可以,第一种我们采用创建一个全局变量的方式来实现,第二种我们使用init包加载的时候创建实例,这里两个都可以,不过根据golang的执行顺序,全局变量的初始化函数会比包的init函数先执行,没有特别的差距。

懒汉式v1

type singleton struct {

}

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

在高并发的时候会有多个线程同时掉这个方法,那么都会检测instance为nil,这样就会导致创建多个对象,完全不可取

懒汉式v2

type singleton struct {

}

var instance *singleton
var lock sync.Mutex

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

这里对整个方法进行了加锁,这种可以解决并发安全的问题,但是效率就会降下来,每一个对象创建时都是进行加锁解锁判断,这样就拖慢了速度。

懒汉式v3

type singleton struct {

}

var instance *singleton
var lock sync.Mutex

func GetInstance() *singleton {
    if instance == nil{ // 代码1
        lock.Lock() // 代码2
        instance = new(singleton)
        lock.Unlock()
    }
    return instance
}

这种方法也是线程不安全的,虽然我们加了锁,多个线程同样会导致创建多个实例,执行完代码1后,执行代码2前,这小段时间别的协程可能已经调用完了GetInstance,已经生成一个实例。 完全不可取

懒汉式v4

type singleton struct {
    
}

var instance *singleton
var lock sync.Mutex

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

双重检查,安全

懒汉式v5

type singleton struct {
    
}

var instance *singleton
var once sync.Once
func GetInstance() *singleton {
    once.Do(func() {
        instance = new(singleton)
    })
    return instance
}

利用golang sync.once只执行一次的特性。

once内部原理

我们要自己实现这么一个功能如何做呢?

定义一个status变量用来描述是否已经执行过了 使用sync.Mutex 或者sync.Atomic实现线程安全的获取status状态, 根据状态判断是否执行特定的函数。 看看官方是怎么实现的:

type Once struct {
	done uint32
	m    Mutex
}


func (o *Once) Do(f func()) {

	if atomic.LoadUint32(&o.done) == 0 { // 代码1
		
		o.doSlow(f)
	}
}

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

done就是标志位,表示是否已经执行过Do内部的函数。

代码1和代码2是双检查,不然代码1和加锁之前可能已经实例了。

问题1:代码1处为啥用原子操作,直接判断o.done不行吗? 不用原子操作是可以的,因为后面代码2处还有一个check,但是通过atomic可以保证在o.done设置为1之后能看到这个设置的结果,避免总是落入到doSlow逻辑中。

问题2:代码2处为啥不用原子操作? 已经在锁中间了,别人现在也改不了o.done,不用原子操作。

问题3:为啥要用锁? 为了执行f()和修改done在一个锁范围内

问题4:这样优化为啥不行?

func (o *Once) Do(f func()) {
	if !atomic.CompareAndSwapUint32(&o.done, 0, 1) {
		return
	}
	f()
}

有人这样提议优化,A协程修改了done,f函数还没执行完,如果是用来单例的话,那么单例还没初始化完,但是B已经能读到done是1了,他自己用单例就会出问题了。