1 单例模式定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点
使用单例模式需要注意三个要点:
- 某个类只能有一个实例
- 这个类必须自行创建这个实例
- 这个类必须自行向整个系统提供这个实例
单例模式写法:
- 饿汉式:类在
*编译时*
创建自己的实例 - 懒汉式:类在
*运行时*
创建自己的实例
依据以上,读者可以自行辨别下面的代码属于哪种写法
2 单例模式的gol语言实现
2.1 不考虑并发情况
package manager
import (
"fmt"
)
var m *Manager
func GetInstance() *Manager {
if m == nil {
m = &Manager{}
}
return m
}
type Manager struct {
}
func (p Manager) Manage() {
fmt.Println("manage...")
}
以上代码虽然实现了单例模式,在非并发的环境下执行,确实没有问题,但是考虑并发的情况,当第一个goroutine执行到m=&Manager{}之前,第二个gotoutine也来获取实例了,第二个gotoutine去判断m是不是等于nil,因为m=&Manager{}还没来得及执行,所以m=nil,那么if中的m = &Manager{}就可能会被执行两遍。
利用go的锁机制sync.Mutex修改以上代码。
2.2 考虑并发的情况
package manager
import (
"fmt"
"sync"
)
var m *Manager
var lock *sync.Mutex = &sync.Mutex{}
func GetInstance() *Manager {
lock.Lock()
defer lock.Unlock()
if m == nil {
m = &Manager{}
}
return m
}
type Manager struct {
}
func (p Manager) Manage() {
fmt.Println("manage...")
}
这样修改之后,保证只有一个goroutine能够执行GetInstance函数,这样并发的问题就解决了,但是现在又有一个问题,每次goroutine来的时候都会被锁挡在GetInstance之外等上一个goroutine执行结束,这样代码执行效率肯定会下降,下面我们引入双重锁机制
类修改我们的代码。
package manager
import (
"fmt"
"sync"
)
var m *Manager
var lock *sync.Mutex = &sync.Mutex{}
func GetInstance() *Manager {
if m == nil {
lock.Lock()
defer lock.Unlock()
if m == nil {
m = &Manager{}
}
}
return m
}
type Manager struct {
}
func (p Manager) Manage() {
fmt.Println("manage...")
}
这样的话就避免了每次gouroutine过来的时候都需要加锁了。有的读者可能会问,为什么我们的代码里面需要执行两次m==nil的判断呢?试想一下当两个goroutine同时通过第一重m==nil判断的时候,由于锁机制,只有一个goroutine继续执行,另一个则在外排队等待,当第一个goroutine执行完毕之后,如果没有第二重m==nil判断的话,第二个goroutine也会继续执行语句m = &Manager{},这样就没有起到单例的目的了。
其实go语言中有一种机制可以保证代码只会被执行一次,而不需要我们手动加锁解锁,也就是go中的sync.Once
,它的Do方法会保证传给它的函数只会被调用一次。
2.3 使用sync.Once改进
package manager
import (
"fmt"
"sync"
)
var m *Manager
var once sync.Once
func GetInstance() *Manager {
once.Do(func() {
m = &Manager{}
})
return m
}
type Manager struct {
}
func (p Manager) Manage() {
fmt.Println("manage...")
}
有没有发现go代码很优雅的就实现了单例模式呢?
参考资料:
[1] 单例模式
[2] Go设计模式之单例模式
[3] 设计模式-单例模式(Go语言描述)