返回
创建于
状态公开

“懒汉式单例”是并发编程中最经典、也最容易被重排坑害的场景之一。下面我会从错误示例到正确 atomic 实现,逐步剖析:


一、什么是懒汉式单例?

懒汉式单例(Lazy Singleton):只有在第一次调用时才会创建实例,节省资源。


二、错误实现(可能被重排)

下面这段是很多人写的“看起来正确”的懒汉式双重检查锁(Double-Checked Locking):

go
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}

看起来好像没问题,但这其实是有严重的重排隐患


三、为什么会出问题?

这行代码:

go
1instance = &Singleton{value: 42}

其实背后可能被重排成三步

  1. 分配内存
  2. 将指针赋给 instance
  3. 初始化 Singleton{}

如果发生重排为:

text
1Step 1: 分配内存
2Step 2: 设置 instance 指针
3Step 3: 初始化 value

那么其他线程看到 instance != nil,但此时结构体内容还没初始化完成,可能读到 脏数据或 0 值


四、解决方案一:使用 sync.Once(推荐方式)

Go 官方推荐直接用 sync.Once 实现懒汉单例,非常安全,防止重排和多次初始化:

go
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 实现一个线程安全、无重排的懒汉式单例:

go
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 + 屏障 防止重排!