【创建型】单例模式

Posted by Liao on 2023-01-17

一、定义

保证一个类仅有一个实例,并提供一个该实例的全局访问点。

二、结构

三、创建方式

1
2
3
4
5
6
7
8
class Singleton {
private:
Singleton(); // 使其私有化,只能自己使用,别人不能使用,故放在private
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
};
1、线程非安全版本
1
2
3
4
5
6
7
Singleton *Singleton::m_instance = nullptr; // 声明为指针,这个是堆对象
Singleton* Singleton::getInstance(){
if(m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
  • 存在问题:如果有A、B两个线程,线程A首次进来判断if(第二行),但此时时间片用完了,轮到B执行if(第二行),这时候A、B线程会创建对象,会出现创建2个对象的情况。
  • 适用场景:单线程
2、线程安全版本,但所的代价过高
1
2
3
4
5
6
7
8
Singleton *Singleton::m_instance = nullptr; // 声明为指针,这个是堆对象
Singleton* Singleton::getInstance(){
Lock lock;
if(m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
  • 存在问题:n个线程的读操作是不需要加锁,对于读操作,加锁是浪费的。在高并发情景不合适。
3、双检查锁版本,但由于内存读写reorder不安全
1
2
3
4
5
6
7
8
9
10
Singleton *Singleton::m_instance = nullptr; // 声明为指针,这个是堆对象
Singleton* Singleton::getInstance(){
if(m_instance == nullptr) { // 锁前检查一次
Lock lock;
if(m_instance == nullptr) {// 锁后检查一次
m_instance = new Singleton(); // 出现reorder
}
}
return m_instance;
}

用于解决A、B两个线程同时进入到第三行,再检查一次,如果为空才创建

一段代码有指令序列,我们以为它会在假想的顺序执行:(第5行)

1、先分配一个内存;

2、调用Singleton的构造器,对刚才分配的内存做初始化;

3、将创建出来指针,即对象真正的内存地址,赋值给m_instance。

  • 存在问题:线程是在指令层次抢时间片,但真正在cpu指令级别的情况,三个步骤不一定按顺序(reorder),有时候实际情况和我们假想的顺序不一样。(如:1,3,2)
    • 如果出现reorder, 这时候A线程创建了对象(实际是原生的一块内存,没有执行构造器)。随后B线程进来直接返回该对象,但其实这个对象是不可用的。
    • 在所有编译器中,如果不加volatile会出问题,并且出问题的概率很高。
    • 通常是由编译器来优化reoder的问题 。

Go版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"log"
"sync"
"time"
)

type Singleton struct {
Name string
}

var (
instance *Singleton
lock sync.Mutex
)

func GetInstance() *Singleton {
if instance == nil {
lock.Lock()
defer lock.Unlock()
if instance == nil {
instance = &Singleton{Name: "zhangsan"}
}
}
return instance
}

func main() {
for i := 0; i < 5; i++ {
go func() {
resp := GetInstance()
log.Printf("the value is : %s", resp.Name)
}()
}
time.Sleep(time.Second)
}

4、双检查锁的正确实现(volatile)

C++11版本之后的跨平台实现(volatile),屏蔽编译器的reorder

四、使用场景

  • 由于性能问题,只能创建一个实例的场景,如数据连接的对象,只需要创建一次,后续对数据库的操作可以复用这个对象。