单例模式,就是在整个进程的过程中,只会存在一个实例。这样做的好处主要是两个:
为了节省资源,有时候会对单例模式进行延迟初始化,因此根据初始化的时间可以分为:饿汉式单例、懒汉式单例。
饿汉式就是在提前初始化,一般在程序启动的时候初始化,很多时候利用编程语言runtime的启动特性来规避并发问题。比如go语言的init函数。
一般实现如下:
// 饿汉单例
package singleton
type EagerSingleton struct{}
var instance *EagerSingleton
func init() {
instance = &EagerSingleton{}
}
func GetInstance() *EagerSingleton {
return instance
}
// 程序入口
package main
import (
"awesomeProject/singleton"
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
instance := singleton.GetInstance()
fmt.Printf("instance:%p\n", instance)
}()
}
wg.Wait()
}
// 输出:所有的地址都是一样的
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
饿汉式单例的优点很明显不会有并发问题,因为在一开始程序初始化的时候就实例就是产生了。缺点也很明显,就是浪费资源,如果一个实例一直没用到,就会造成资源的浪费。
所以,为了更好的节约资源,引入了懒汉式单例。
也就是将实例初始化的时机由程序初始化的时候改为了使用到的时候在初始化,这样做的好处有以下两点:
一般实现如下:
// 懒汉式单例
package singleton1
type LazySingleton struct {}
var instance *LazySingleton
func GetInstance() *LazySingleton {
if instance == nil {
instance = &LazySingleton{}
}
return instance
}
// 程序入口
package main
import (
"awesomeProject/singleton1"
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
fmt.Printf("instance: %p\n", singleton1.GetInstance())
}()
}
wg.Wait()
}
// 在使用go run -race ./main.go执行之后,如图
如果使用 -race失败,可以参考我的另一篇文章:使用race错误
可以看到是发生了data race,也就是并发会产生问题:具体就是:
func GetInstance() *LazySingleton {
if instance == nil {
instance = &LazySingleton{}
}
return instance
}
// 在instance=nil判断的是否不是原子性的,可能存在两个goroutine同时走到这一块,然后都进入到条件内,从而导致并发问题。
// 饿汉式单例
import "sync"
type LazySingleton struct{}
var (
instance *LazySingleton
once sync.Once
)
func GetInstance() *LazySingleton {
once.Do(func() {
instance = &LazySingleton{}
})
return instance
}
// 再次使用go run -race ./main.go,结果如图:
多次运行也不会产生问题:
但是!!!,需要注意的是,如果sync.Once使用的时候Do函数失败了,那么就会导致对象一直为nil。比如:
// 饿汉式单例
package singleton1
import "sync"
type LazySingleton struct{}
var (
instance *LazySingleton
once sync.Once
)
func NewLazySingleton(testMsg string) *LazySingleton {
if testMsg == "Deny" {
return nil
}
return &LazySingleton{}
}
func GetInstance() *LazySingleton {
once.Do(func() {
instance = NewLazySingleton("Deny")
})
return instance
}
// 程序入口
package main
import (
"awesomeProject/singleton1"
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(100)
msg := []string{"Deny", "Allow", "Limit"}
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
testMsg := msg[rand.Intn(len(msg))]
fmt.Printf("instance: %p\n", singleton1.GetInstance(testMsg))
}()
}
wg.Wait()
}
如果第一次执行的时候,随机到了Deny就会导致后续一直是nil,限制太大,我们继续看看sync.mutex的实现。
// 饿汉式单例
package singleton1
import "sync"
type LazySingleton struct{}
var (
instance *LazySingleton
mutex sync.Mutex
)
func NewLazySingleton(testMsg string) *LazySingleton {
if testMsg == "Deny" {
return nil
}
return &LazySingleton{}
}
func GetInstance(test string) *LazySingleton {
mutex.Lock()
defer mutex.Unlock()
if instance == nil {
instance = NewLazySingleton(test)
}
return instance
}
// 程序入口
package main
import (
"awesomeProject/singleton1"
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(100)
msg := []string{"Deny", "Allow", "Limit"}
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
testMsg := msg[rand.Intn(len(msg))]
fmt.Printf("instance: %p\n", singleton1.GetInstance(testMsg))
}()
}
wg.Wait()
}
这样即使有一次因为deny为nil,也不会影响后续的执行。但是这种方式仔细想想的话,不管instance有没有被初始化,每次有一个instance来的时候都需要获取一次锁,这样对性能很不友好。因此,可以在此基础上加一层判断,如果instance已经被初始化了,就没必要再获取锁了。
func GetInstance(test string) *LazySingleton {
if instance == nil {
mutex.Lock()
defer mutex.Unlock()
if instance == nil {
instance = NewLazySingleton(test)
}
}
return instance
}
有兴趣的可以通过进行一些性能对比测试。
主要就是将instance==nil
换成了原子引用写法
package singleton1
import (
"sync"
"sync/atomic"
)
type LazySingleton struct{}
var (
instanceRef atomic.Value
lock sync.Mutex
)
func NewLazySingleton(testMsg string) *LazySingleton {
if testMsg == "Deny" {
return nil
}
return &LazySingleton{}
}
func GetInstance(test string) *LazySingleton {
if instanceRef.Load() == nil {
lock.Lock()
defer lock.Unlock()
if instanceRef.Load() == nil {
instanceRef.Store(NewLazySingleton(test))
}
}
return instanceRef.Load().(*LazySingleton)
}
直接去掉锁的影响
package singleton1
import (
"sync/atomic"
)
type LazySingleton struct{}
var (
instanceRef atomic.Value
)
func NewLazySingleton(testMsg string) *LazySingleton {
if testMsg == "Deny" {
return nil
}
return &LazySingleton{}
}
func GetInstance(test string) *LazySingleton {
if instanceRef.Load() != nil {
return instanceRef.Load().(*LazySingleton)
}
newInstance := NewLazySingleton(test)
// 如果需要手动释放资源,那么需要判断该cas的返回值,这里就不多说。
instanceRef.CompareAndSwap(nil, newInstance)
return instanceRef.Load().(*LazySingleton)
}
从上面也可以看得出来:
优点:
访问控制;节约资源
缺点:
职责过重,不好扩展。