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了,他自己用单例就会出问题了。