返回
创建于
状态公开
“懒汉式单例”是并发编程中最经典、也最容易被重排坑害的场景之一。下面我会从错误示例到正确 atomic 实现,逐步剖析:
一、什么是懒汉式单例?
懒汉式单例(Lazy Singleton):只有在第一次调用时才会创建实例,节省资源。
二、错误实现(可能被重排)
下面这段是很多人写的“看起来正确”的懒汉式双重检查锁(Double-Checked Locking):
1package main
2
3import (
4 "fmt"
5 "sync"
6)
7
8type Singleton struct {
9 value int
10}
11
12var instance *Singleton
13var mu sync.Mutex
14
15func GetInstance() *Singleton {
16 if instance == nil { // 第一次检查
17 mu.Lock()
18 defer mu.Unlock()
19 if instance == nil { // 第二次检查
20 instance = &Singleton{value: 42}
21 }
22 }
23 return instance
24}
25
26func main() {
27 var wg sync.WaitGroup
28 for i := 0; i < 10; i++ {
29 wg.Add(1)
30 go func() {
31 s := GetInstance()
32 fmt.Println(s.value)
33 wg.Done()
34 }()
35 }
36 wg.Wait()
37}看起来好像没问题,但这其实是有严重的重排隐患!
三、为什么会出问题?
这行代码:
1instance = &Singleton{value: 42}其实背后可能被重排成三步:
- 分配内存
- 将指针赋给
instance - 初始化
Singleton{}
如果发生重排为:
1Step 1: 分配内存
2Step 2: 设置 instance 指针
3Step 3: 初始化 value那么其他线程看到 instance != nil,但此时结构体内容还没初始化完成,可能读到 脏数据或 0 值!
四、解决方案一:使用 sync.Once(推荐方式)
Go 官方推荐直接用 sync.Once 实现懒汉单例,非常安全,防止重排和多次初始化:
1package main
2
3import (
4 "fmt"
5 "sync"
6)
7
8type Singleton struct {
9 value int
10}
11
12var instance *Singleton
13var once sync.Once
14
15func GetInstance() *Singleton {
16 once.Do(func() {
17 instance = &Singleton{value: 42}
18 })
19 return instance
20}
21
22func main() {
23 var wg sync.WaitGroup
24 for i := 0; i < 10; i++ {
25 wg.Add(1)
26 go func() {
27 s := GetInstance()
28 fmt.Println(s.value)
29 wg.Done()
30 }()
31 }
32 wg.Wait()
33}特点:
sync.Once内部实现了 atomic 和 memory barrier,防止重排- 保证只初始化一次
- 线程安全,推荐用法
五、解决方案二:使用 atomic 手动实现(理解重排核心)
如果你想深入了解原理,也可以自己用 atomic 实现一个线程安全、无重排的懒汉式单例:
1package main
2
3import (
4 "fmt"
5 "sync"
6 "sync/atomic"
7 "unsafe"
8)
9
10type Singleton struct {
11 value int
12}
13
14var rawInstance unsafe.Pointer
15var mu sync.Mutex
16
17func GetInstance() *Singleton {
18 p := atomic.LoadPointer(&rawInstance) // acquire屏障
19 if p == nil {
20 mu.Lock()
21 defer mu.Unlock()
22 p = atomic.LoadPointer(&rawInstance)
23 if p == nil {
24 instance := &Singleton{value: 42}
25 atomic.StorePointer(&rawInstance, unsafe.Pointer(instance)) // release屏障
26 p = unsafe.Pointer(instance)
27 }
28 }
29 return (*Singleton)(p)
30}
31
32func main() {
33 var wg sync.WaitGroup
34 for i := 0; i < 10; i++ {
35 wg.Add(1)
36 go func() {
37 s := GetInstance()
38 fmt.Println(s.value)
39 wg.Done()
40 }()
41 }
42 wg.Wait()
43}核心:
atomic.LoadPointer+atomic.StorePointer是 acquire/release 屏障组合,阻止指令重排unsafe.Pointer是对泛型*T的底层封装(在 Go 没有泛型之前常用)
六、总结
| 方法 | 是否安全 | 是否会被重排 | 复杂度 | 推荐 |
|---|---|---|---|---|
| 普通双重检查 | 否 | 是 | 低 | ❌ |
sync.Once | 是 | 否 | 低 | ✅ 推荐 |
| atomic + unsafe | 是 | 否 | 高 | 进阶用法 |
七、一句话总结
Go 实现懒汉式单例时,最好使用
sync.Once,如果必须手动实现,要用atomic.Store/Load + 屏障防止重排!