概述
单例是一种常用的模式。单例就是这个实例在应用的整个声明周期只有一个实例。单例在实现时根据初始化方式可以分为两类:
- 懒汉
- 饿汉
懒汉模式就是懒加载,在获取实例时才创建;饿汉就是程序启动时就创建实例。
个人建议能使用饿汉就使用饿汉,因为实例有什么问题可以提前发现和解决,别等到运行了一段时间发现实例有问题。但是实在不能用就用懒汉嘛,可以进行各种测试保证。
接下来,我将带大家探索在 Golang 中实现单例模式的五种方式。
一、懒汉模式 (简单实现)
在这个版本中,我们将创建一个实例,并在全局公开这个实例。当我们第一次试图获取这个单例的时候, 它将为我们创建一个。
在我们再次尝试获取它时,它将返回第一次创建的实例。
在以下代码中,首次调用 GetLazyInstance
创建并返回一个新的 LazySingleton
实例,随后的调用将会返回首次创建的实例。
package main
import "fmt"
var lazyinstance *LazySingleton
type LazySingleton struct {
}
func GetLazyInstance() *LazySingleton {
if lazyinstance == nil {
lazyinstance = &LazySingleton{}
}
return lazyinstance
}
func main() {
s1 := GetLazyInstance()
s2 := GetLazyInstance()
if s1 == s2 {
fmt.Println("s1 == s2") // 输出: s1 == s2
} else {
fmt.Println("s1 != s2")
}
}
然而这个版本的实现仅在单线程环境下工作,在高并发的环境下可能会创建多个 LazySingleton
实例,这就违反了单例模式的原则了。
二、懒汉模式的线程安全型实现
为了解决懒汉模式在高并发环境下的问题,我们可以通过添加一个互斥锁 mux
来保证并发情况下的实例唯一性。
以下是加入互斥锁的实现版本:
package main
import "sync"
type LockInstance struct {
s string
}
var mux sync.Mutex
var lockinstance *LockInstance
func GetLockInstance() *LockInstance {
if lockinstance == nil {
mux.Lock()
defer mux.Unlock()
println("create singleton instance")
if lockinstance == nil {
lockinstance = &LockInstance{
s: "lock",
}
}
}
return lockinstance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
instance := GetLockInstance()
println(instance.s)
}()
}
wg.Wait()
}
这个版本的实现已经是线程安全的,但多线程环境下性能会有点不太好,因为每次获取实例操作都要获取锁。
不了解sync.Mutex的同学可以参考我的文章:逐步学习Go-sync.Mutex(详解与实战)
我们是不是有更好的办法?这就是接下来要说的第三种实现方式。
三、懒汉模式(使用 sync.Once
惰性实现)
这是最为推荐的一种实现方式。通过使用 sync.Once
类型,我们可以保证懒加载并且线程安全。 once.Do()
保证其中的函数只会执行一次。
package main
import "sync"
type ConcurrentSingleton struct {
}
var once sync.Once
var concurrentinstance *ConcurrentSingleton
func GetConcurrentInstance() *ConcurrentSingleton {
once.Do(func() {
println("init only once")
concurrentinstance = &ConcurrentSingleton{}
})
return concurrentinstance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
instance := GetConcurrentInstance()
println(instance)
}()
}
wg.Wait()
}
看!在所有并发下,只初始化一次,而且拥有很好的性能。
不了解sync.Once的同学可以参考我的文章: 逐步学习Go-sync.Once(只执行一次)Exactly Once
四、饿汉模式
在饿汉模式中,单例在程序开始的时候就完成了初始化。这种方式的优点是加载速度快,线程安全,因为它没有在运行时进行多余的判断,但是对应的缺点就是不管该类是否被使用,先进性初始化了。
package main
type Singleton struct {
s string
}
var instance *Singleton
func init() {
instance = &Singleton{s: "hello"}
}
func GetInstance() *Singleton {
return instance
}
func main() {
println(GetInstance().s)
}
五、饿汉模式 (另一种实现)
这是另一种饿汉模式的实现,这种实现更加简洁,并且仍然保持了只有一个单例实例的原则。
package main
import "fmt"
type EagerInstance struct {
}
var eagerInstance = &EagerInstance{}
func GetEagerInstance() *EagerInstance {
return eagerInstance
}
func main() {
s3 := GetEagerInstance()
s4 := GetEagerInstance()
if s3 == s4 {
fmt.Println("s3 == s4")
} else {
fmt.Println("s3 != s4")
}
}
总结
目前我只探索到这五种单例方式,如果你还有更好的想法和方式,欢迎讨论。