单例模式大家都比较了解,定义如下:一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
单例模式虽然理解起来比较简单,但是真正实现的时候有很多细节需要考虑,一般考虑点有如下几个:
- 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例
- 考虑对象创建时的线程安全问题
- 考虑是否支持延迟加载
- 考虑 getInstance() 性能是否高(是否加锁)
代码
语法不同,对于这几点关注程度也不同。对于Go语言,在这里提供一种写法,使用sync.Once.Do。该函数的作用是只执行一次。
所以我们可以这么写:
type Single struct {
}
var (
once sync.Once
single *Single
)
func GetSingleInstance() *Single {
once.Do(func() {
single = &Single{}
})
return single
}
无论多少请求,只会有一个Single的实例。
测试
现在我们思考一个场景,如果突然有100个请求同时请求GetSingleInstance接口,这些请求是会等Do执行完还是无视Do直接return single呢?
理论上是需要等Do执行完的,否则返回的single为空,会导致严重错误。虽然是这么想,不过还是做个测试吧。
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
func main() {
var once sync.Once
onceBody := func() {
fmt.Println("Only once start")
time.Sleep(time.Second * 5)
fmt.Println("Only once end")
}
for i := 0; i < 5; i++ {
j := i
go func(int) {
fmt.Println(j)
once.Do(onceBody)
fmt.Println("lll" + strconv.Itoa(j))
}(j)
}
fmt.Println("finished")
time.Sleep(time.Second * 10)
}
测试方案很简单,启动5个goroutine,同时调用Do,onceBody设置sleep 5秒,只要检查输出,就能判断是否会阻塞。
执行结果如下:
➜ myproject go run main.go
finished 0
Only once start
2 4 3 1
Only once end
lll2 lll4 lll0 lll1 lll3
可以看出,只有Do执行完毕后,所有goroutine才会输出,证明Do都会被调用,但只有一个会真正执行,在真正的执行完前,其它goroutine会被阻塞。
其实这里有一个隐藏风险,如果Do执行的函数很耗时,会导致大量goroutine累积,编程的时候需要考虑到这一点。
具体实现
Do这个功能是如何实现的呢?让我们看一下源码:
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
多个协程查看done值为0,进入doSlow,只有一个协程会获得锁,其它协程别阻塞。
其实用到的都是比较常规的技术,主要是互斥锁、信号量、defer,但是设计上还是很巧妙的。这也是Go的一个优势,解决冲突使用锁,快速、安全、方便,但是需要考虑好性能问题。
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:
往期文章回顾:
技术
- Go设计模式(4)-代码编写
- Go设计模式(3)-设计原则
- Go设计模式(2)-面向对象分析与设计
- 支付接入常规问题
- HTTP2.0基础教程
- Go设计模式(1)-语法
- MySQL开发规范
- HTTPS配置实战
- Go通道实现原理
- Go定时器实现原理
- HTTPS连接过程
- 限流实现2
- 秒杀系统
- 分布式系统与一致性协议
- 微服务之服务框架和注册中心
- Beego框架使用
- 浅谈微服务
- TCP性能优化
- 限流实现1
- Redis实现分布式锁
- Golang源码BUG追查
- 事务原子性、一致性、持久性的实现原理
- CDN请求过程详解
- 常用缓存技巧
- 如何高效对接第三方支付
- Gin框架简洁版
- InnoDB锁与事务简析
- 算法总结
读书笔记
- 原则
- 资治通鉴
- 敏捷革命
- 如何锻炼自己的记忆力
- 简单的逻辑学-读后感
- 热风-读后感
- 论语-读后感
- 孙子兵法-读后感
思考
- 反对自由主义
- 实践论
- 评价自己的标准
- 服务端团队假期值班方案
- 项目流程管理
- 对项目管理的一些看法
- 对产品经理的一些思考
- 关于程序员职业发展的思考
- 关于代码review的思考
- Markdown编辑器推荐-typora