文章目录
单例模式
GoF 给出的单例模式的定义:
保证一个类,只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。
其实提到单例模式,只要我们接触过 Web 项目的学习,大概率都会和数据库/Redis 缓存打交道,此时我们就很有可能需要在 main 函数当中初始化一个与数据库对象交互的实例,它通常是一个线程安全的指针,这就是单例模式的一个典型应用,即:提供一个可供客户端使用的唯一对象,该对象仅提供对外操作的接口,并隐藏了初始化等其他细节,该对象应该保证线程安全,实现幂等性。
单例模式中的角色职责
Singleton(单例):单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()
工厂方法,这是客户端可以访问该类的唯一实例。为了防止在外部对其实例化,将其构造函数设计为私有。在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。
一个单例模式的 Demo 如下述代码段所示:
package main
import "fmt"
/*
1. 对于单例模式的类而言, 只能有一个实例
2. 必须自行创建这个实例
3. 必须向整个系统提供这个实例
与数据库交互的对象就是典型的单例模式实例
*/
// 1. 保证这个类非公有化, 外界不能通过这个类直接创建对象
// 因此这个类的定义应该是非导出的(首字母非大写)
type singleton struct {}
// 2. 需要有一个指针指向这个唯一的对象, 且这个指针不能改变引用对象
// Golang 中没有常量指针的概念, 所以只能通过将指针私有化来避免外部模块访问
var instance *singleton = new(singleton)
// 3. 必须向整个系统提供这个实例, 通过函数来完成
func GetInstance() *singleton {
return instance
}
func (s *singleton) SomeThing() {
fmt.Println("A singleton Method")
}
func main() {
s := GetInstance() // 获取指针
s.SomeThing()
}
《Easy 搞定 Golang 设计模式》中将上述方法命名为“饿汉式”的单例模式,原因是上述代码在初始化单例的唯一指针时,就为其开辟好了一个对象,不会出现线程并发创建,导致多个单例出现,这种模式的缺点也很明显,那就是即使这个对象在业务当中没有使用,也会客观创建一块内存。与之相对的模式是“懒汉式”,即“懒创建”,只有在第一次调用GetInstance()
时才会创建实例:
/*... ... ...*/
func GetInstance() *singleton {
if instance == nil {
instance = new(singleton)
return instance
}
return instance
}
/*... ... ...*/
线程安全的单例模式
上面的“懒汉式”单例调用仍然不是线程安全的设计方式,如果多个线程或协程同时首次调用GetInstance()
方法导致多个实例创建,则违背了单例的设计初衷。我们可以使用 Golang 自带的 Mutex,为“懒汉式”单例的创建加锁,从而保证线程安全。
线程安全最大的缺点在于每次调用都需要加锁,在性能上不够高效,具体的实现如下:
package main
import (
"fmt"
"sync"
)
var lock sync.Mutex
type singleton struct{}
var instance *singleton
func GetInstance() *singleton {
lock.Lock()
defer lock.Unlock()
if instance == nil {
return new(singleton)
} else {
return instance
}
}
func (s *singleton) SomeThing() {
fmt.Println("SomeThing is called")
}
func main() {
s := GetInstance()
s.SomeThing()
}
可以借助sync/atomic
来进行内存的状态存留做到互斥。atomic
可以自动加载和设置标记,代码如下:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var initialized uint32
var lock sync.Mutex
type singleton struct{}
var instance *singleton
func GetInstance() *singleton {
if atomic.LoadUint32(&initialized) == 1 {
return instance
}
lock.Lock()
defer lock.Unlock()
if initialized == 0 {
instance = &singleton{}
atomic.StoreUint32(&initialized, 1)
}
return instance
}
func (s *singleton) SomeThing() {
fmt.Println("Something has been called")
}
func main() {
s := GetInstance()
s.SomeThing()
}
针对上述逻辑,Golang 已经使用Once
模块进行了优化:
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
使用Once
来实现单例模式:
package main
import (
"fmt"
"sync"
)
var once sync.Once
type singleton struct{}
var instance *singleton
func GetInstance() *singleton {
once.Do(func() {
instance = new(singleton)
})
return instance
}
func (s *singleton) SomeThing() {
fmt.Println("Something has been called")
}
func main() {
s := GetInstance()
s.SomeThing()
}
单例模式的优缺点
优点:
- 单例模式提供了对唯一实例的受控访问;
- 节约系统资源。在系统内存中只存在一个对象。
缺点:
- 拓展略难,单例模式中没有抽象层;
- 单例类的职责过重。
适用场景
- 系统只需要一个实例对象,如系统提供一个唯一的序列号生成器(比如 Snowflake 算法)或资源管理器,或需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。