单例模式的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实现

  1. 按照php的思路,不考虑多协程并发,分析下会有什么问题
// 不考虑并发的单例
// 包级别的变量(不管是导出还是未导出)作用域都是全局的,全局变量可以被多个协程同时访问
var singletonInstance *Singleton 

type Singleton struct {
}

func NewSingleton() *Singleton {
    // 这一步可能被并发执行,导致最终的singletonInstance不确定是被哪个协程创建的
	if singletonInstance == nil { 
		singletonInstance = &Singleton{}
	}
	return singletonInstance
}
  1. 为了解决并发的问题,借助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
}
  1. 使用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个问题:

  1. atomic.CompareAndSwapUint32,为什么这里不能使用cas方式的原子操作?

    源码注释其实已经解释清楚了。cas是原子性的,假如两个并行的协程同时执行到cas语句发生争抢,第一个协程获胜先执行cas,第二个协程等第一个执行完cas之后o.done的值已经被改变了,所以会直接返回。但是这个时候第一个协程有可能还没有执行完f函数,所以第二个协程无法获取f函数的执行结果。

  2. 为什么一定要使用互斥锁?

    这个问题其实跟第一个问题一样,原子锁只能保证对某个具体变量的操作是原子性的,而互斥锁可以保证多个语句的执行是原子性的。

  3. 为什么第一次判断使用了原子读取,第二次判断是普通的读取?

    原子操作主要是防止多个协程同时读写同一变量,而导致数据异常,我们应该养成这样一种习惯,当一个变量可能被多个协程同时操作时,应该使用原子操作。但是在这种场景下,我觉得第一步的判断是不是原子读取无所谓,因为最终都会通过互斥锁保证原子性。第二步的读取操作本身就是在锁内发生,所以无需再加原子锁。