Go设计模式 - 单例模式
单例模式的php实现
单例模式在所有设计模式中算是最简单的了,主要是保证一个类始终只有一个实例。在php中因为是单线程,所以实现起来非常简单:“三私一公”,私有化静态属性,私有化克隆方法,私有化构造方法,然后创建一个公有的静态方法做实例化的操作。代码示例:
class Singleton {
private static $instance = null;
private function __clone(){}
private function __construct(){}
public static function GetInstance(){
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
}
$a = Singleton::GetInstance();
单例模式的golang实现
- 按照php的思路,不考虑多协程并发,分析下会有什么问题
// 不考虑并发的单例
// 包级别的变量(不管是导出还是未导出)作用域都是全局的,全局变量可以被多个协程同时访问
var singletonInstance *Singleton
type Singleton struct {
}
func NewSingleton() *Singleton {
// 这一步可能被并发执行,导致最终的singletonInstance不确定是被哪个协程创建的
if singletonInstance == nil {
singletonInstance = &Singleton{}
}
return singletonInstance
}
- 为了解决并发的问题,借助sync.Mutex包进行加锁
// 通过互斥锁解决并发问题
var singletonInstance *Singleton
var singletonMutex sync.Mutex
type Singleton struct {
}
func NewSingleton() *Singleton {
// 第一次判断:加锁前的判断,是为解决性能问题。
if singletonInstance != nil {
return singletonInstance
}
// 加锁:保证并发安全,让多个协程串行执行
singletonMutex.Lock()
defer singletonMutex.Unlock()
// 第二次判断:加锁之后还需进行判断,因为有可能有多个协程先后执行到这里
if singletonInstance == nil {
singletonInstance = &Singleton{}
}
return singletonInstance
}
- 使用sync.Once包实现
var singletonInstance *Singleton
var singletonOnce sync.Once
type Singleton struct {
}
// 代码相当简洁,无需判断,也无需手动加锁,因为once包已经帮我们做了
func NewSingleton() *Singleton {
singletonOnce.Do(func() {
singletonInstance = &Singleton{}
})
return singletonInstance
}
sync.Once解析
sync.Once只有一个函数Do(f func()),在并发场景下能够保证f只精确的执行一次。
Once的源码很少,如下所示,关键的注释翻译成了中文。实现原理也是两步判断:加锁前的判断和加锁后的判断。不同点在于判断使用的是atomic包的原子操作。
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
// 错误的实现方案:
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
// f()
// }
//
// Do保证return时,f函数已经执行完成
// 但是cas实现策略将不会做出以下保证:
// 两个并行的调用,获胜的将会执行f,另外一个不等第一个调用完成就直接返回。
// 这就是为什么要回退到互斥锁,并且atomic.StoreUint32必需在f之后执行。
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()
}
}
通过分析源码以及注释,仔细分析一下3个问题:
atomic.CompareAndSwapUint32,为什么这里不能使用cas方式的原子操作?
源码注释其实已经解释清楚了。cas是原子性的,假如两个并行的协程同时执行到cas语句发生争抢,第一个协程获胜先执行cas,第二个协程等第一个执行完cas之后o.done的值已经被改变了,所以会直接返回。但是这个时候第一个协程有可能还没有执行完f函数,所以第二个协程无法获取f函数的执行结果。
为什么一定要使用互斥锁?
这个问题其实跟第一个问题一样,原子锁只能保证对某个具体变量的操作是原子性的,而互斥锁可以保证多个语句的执行是原子性的。
为什么第一次判断使用了原子读取,第二次判断是普通的读取?
原子操作主要是防止多个协程同时读写同一变量,而导致数据异常,我们应该养成这样一种习惯,当一个变量可能被多个协程同时操作时,应该使用原子操作。但是在这种场景下,我觉得第一步的判断是不是原子读取无所谓,因为最终都会通过互斥锁保证原子性。第二步的读取操作本身就是在锁内发生,所以无需再加原子锁。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 喵了个咪!