单例模式

一、单例模式实现

1. 线程不安全版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
public:
static Singleton* getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
private:
Singleton() = default; // 私有构造函数
public:
Singleton(const Singleton& other) = delete;
Singleton& operator=(const Singleton& other) = delete;
private:
static Singleton* m_instance;
};

这是最经典的实现方式,采用的是懒初始化的方式,但是在多线程的情况下,这种方式是不安全的。因为 m_instance = new Singleton() 不是一个原子操作,有可能在一个线程 new Singleton() ,还没有赋值给 m_instance 变量时,另外一个线程过来发现 m_instancenullptr ,又去初始化了。这就导致产生两个对象。

2. 加锁的版本

1
2
3
4
5
6
7
static Singleton* getInstance() {
std::lock_guard<std::mutex> lck(mtx);
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}

加锁之后,保证了线程安全,但是每次获取对象的时候,都需要加锁,导致存在性能问题。

3. 双检查锁(指令重排问题)

1
2
3
4
5
6
7
8
9
static Singleton* getInstance() {
if (m_instance == nullptr) {
std::lock_guard<std::mutex> lck(mtx);
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}

如上的 double-checked locking 双检查锁的思想,看起来很棒,只有在第一次需要的时候才会使用锁。但是直到 2000 年的时候才发现漏洞。因为内存读写的乱序执行(编译器的问题)(指令重排)

m_instance = new Singleton() 这句话会被分成三个步骤来执行:

  • 分配了一个 Singleton 类型对象所需要的内存
  • 在分配的内存处构造 Singleton 类型的对象
  • 把分配的内存的地址赋给指针 m_instance

实际上,上面的这三个步骤中,步骤 2 和步骤 3 不一定是按序执行的。如果某个线程执行 m_instance = new Sinleton() 的时候按照步骤 1、3、2 的顺序执行,当执行完步骤 3 ,给 Sinelton 类型变量对象分配了内存并赋给了 m_instance 变量,此时 m_instance 不为 nullptr,但没有初始化。此时,其他线程执行 getInstance() 得到一个对象,但这个对象并没有真正的被构造。就会出现 bug。

4. 解决指令重排问题

Java 和 c# 发现了指令重排问题后,就加了一个关键字 volatile,在声明 m_instance 变量的时候,加上 volatile 修饰,编译器就会按照顺序执行(一定先分配内存、在执行构造、都完成之后再赋值)。到了 c++11 版本,终于有了内存栅栏这样的机制帮助我们解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Singleton* getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
std::atomic_thread_fence(std::memory_order_release);
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}

5. linux 平台的 pthread_once 函数

在 linux 中,pthread_once() 函数可以保证某个函数只执行一次。

1
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

这个函数使用初始值为 PTHREAD_ONCE_INITonce_control 变量保证 init_routine() 函数在本进程执行序列中仅执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Singleton {
public:
static Singleton* getInstance() {
// int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
pthread_once(&m_once, &Singleton::init);
return m_instance;
}
private:
Singleton() = default;
// 要写成静态方法的原因:类成员函数隐含传递 this 指针
static void init() {
m_instance = new Singleton();
}
public:
Singleton(const Singleton& other) = delete;
Singleton& operator=(const Singleton& other) = delete;
private:
static pthread_once_t m_once;
static Singleton* m_instance;
};

pthread_once_t Singleton::m_once = PTHREAD_ONCE_INIT;
Singleton* Singleton::m_instance = nullptr;

6. c++11 版本简洁的跨平台方案

如上,例子 4 的方案有点麻烦,例子 5 的方案不能跨平台。c++11 已经为我们提供了 std::call_once 方法来保证函数在多线程环境中只被调用一次,而且支持跨平台。而且 c++ 的局部静态变量不仅只会初始化一次,而且还是线程安全的

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
public:
// 注意返回的是引用
static Singleton& getInstance() {
static Singleton m_instance;
return m_instance;
}
private:
Singleton() = default;
public:
Singleton(const Singleton& other) = delete;
Singleton& operator=(const Singleton& other) = delete;
};

如上写法比较简洁,但是注意:

  • gcc 4.0 之后的编译器才支持这种写法
  • c++11 之前不能这么写

大佬关于单例模式的看法:https://www.cnblogs.com/loveis715/archive/2012/07/18/2598409.html