在日常业务开发中,适当应用设计模式,可以实现我们的业务需求,例如全局唯一配置,这里就需要用到单例模式。
单例模式作为创建型模式的一种,我们在什么情况下,可以使用单例模式呢?
这得考虑该模式的适用场景:
- 用来控制类型实例的数量的,当需要确保一个类型只有一个实例
单例模式的适用场景:
- 1.统计当前在线人数(网站计数器):用一个全局对象来记录。
- 2.数据库连接池(控制资源):一般是采用单例模式,因为数据库连接是一种连接数据库资源,不易频繁创建和销毁,如缓存或者数据库等连接池对象。
- 3.应用程序的日志(资源共享):一般日志内容是共享操作,需要在后面不断写入内容所以通常单例设计。
- 4.应用配置:全局唯一实例。
- 5.定义唯一属性:如特定的字符串。
- 6.多线程的线程池设计。
单例模式优缺点
优点
- 1.减少内存开销,尤其是频繁的创建和销毁实例。
- 2.避免对资源过多占用。
缺点
- 1.没有抽象层,不能继承扩展很难。
- 2.违背了“单一职责原则”,一个类只重视内部关系,而忽略外部关系。
- 3.不适用于变化对象。
- 4.滥用单例会出现一些负面问题:如连接池溢出,长时间不被使用,被GC。
单例模式的实现模式
在单例模式中,通常有懒汉模式和饿汉模式,区别在于是否程序启动时就创建实例。
饿汉模式
程序初始化时即创建,可能即使创建不一定使用,造成浪费,但可以及早暴露问题,定位问题。
该模式下的实现细节:
- 1.单例类和构造方法不可导出,避免外部直接获取。
- 2.全局变量中声明。
- 3.初始化时,生成相关实例,并提供唯一外部接口获取实例。
代码实现:
package hungry
// 不可导出的变量、结构体、构造函数
type singletonHungry struct{}
var s *singletonHungry
func init() {
s = &singletonHungry{}
}
// 唯一外部接口
func GetInstance() *singletonHungry {
return s
}
懒汉模式
真正使用时再创建,需要注意并发安全问题,比如实例还未创建,有2个线程同时访问,这里就需要控制只会创建一个实例,通常来说可以通常加互斥锁实现,但也带来一定的性能问题,有时也需要通过 double check 解决。 该模式的实现细节:
- 1.单例类和构造方法不可导出,避免外部直接获取。
- 2.全局变量中声明。
- 3.第一次创建时,通过GetInstance() xxx 创建实例。
比较优雅的 golang 实现中,我们可以通过 sync.Once
提供的接口实现,或者借助于 sync.Mutex + atomic.Load/atomic.Store
实现,二者实现是类似的,都能很好解决并发安全且只有一个实例的问题。
代码实现:
package lazy
import (
"sync"
"sync/atomic"
)
type singletonLazy struct{}
var s *singletonLazy
var once sync.Once
func GetInstance() *singletonLazy {
once.Do(func() {
s = &singletonLazy{}
})
return s
}
var done uint32
var mu sync.Mutex
func GetInstanceV2() *singletonLazy {
// 原子操作,减少锁开销,类似于sync.Once的处理流程
if atomic.LoadUint32(&done) == 1 {
return s
}
mu.Lock()
defer mu.Unlock()
if atomic.LoadUint32(&done) == 0 {
atomic.StoreUint32(&done, 1)
s = &singletonLazy{}
}
return s
}
参考文档: