今天学习设计模式的时候看到了Java方面的双检式单例模式,由于JVM的指令重排序的问题,又在双检式的情况增添了更多的复杂性,于是我就去看看在Golang中是如何实现单例模式的。其实Golang中实现线程安全,同时又能够支持并发访问的方法也是双检法,他的复杂度封装在了sync包中的Once类中,也是通过采用Check -> Lock -> Check的方式来实现的,具体的代码如下

package sync

import (
	"sync/atomic"
)

// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
//
// In the terminology of the Go memory model,
// the return from f “synchronizes before”
// the return from any call of once.Do(f).
type Once struct {
	done uint32
	m    Mutex
}

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 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 == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

可以看到Once类的Do方法通过传递一个匿名函数,这个匿名函数可以用来实现某个类的实例化过程。通过原子操作第一次检查类是否被实例化,如果没有实例化那么就取锁,取到锁后再次通过标志位检查类是否被实例化。这里有人会问了:那么为什么取到锁之后还要再次检查标志位呢?这里的回答是这样的:因为有可能两个协程并发通过了外层的检查,取到锁后没有二次检查就实例化了类,这样会造成多次重复实例化类,造成资源浪费。

那我们接下来看看使用sync.Once如何实例化单例:

package Singleton

import "sync"

/**
此包用于展示通过golang实现一个线程安全的单例模式, 懒汉式的线程安全的单例模式
 */

type singleton struct{}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
	once.Do(func() {
		instance = &singleton{}
	})
	return instance
}