Singleton--C++

文章目录
  1. 1. 懒汉版
    1. 1.1. 最简版
    2. 1.2. 使用静态的嵌套类对象
    3. 1.3. 加锁
    4. 1.4. 双检查锁,由于内存读写导致不安全
    5. 1.5. local static
    6. 1.6. C++11 call_once
  2. 2. 饿汉版

懒汉版

最简版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
private:
static Singleton* instance;
Singleton() {};
~Singleton() {};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton* getInstance() {
if(instance == NULL) instance = new Singleton();
return instance;
}
};

Singleton* Singleton::instance = NULL;

存在内存泄漏的问题,解决方案:

  1. 使用智能指针
  2. 使用静态的嵌套类对象

使用静态的嵌套类对象

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 {
private:
static Singleton* instance;
Singleton() {};
~Singleton() {};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton* getInstance() {
if(instance == NULL) instance = new Singleton();
return instance;
}
private:
class Deletor {
public:
~Deletor() {
if(Singleton::instance != NULL) delete Singleton::instance;
}
};
static Deletor deletor;
};

Singleton* Singleton::instance = NULL;

在程序运行结束时,系统会调用静态成员 deletor 的析构函数,该析构函数会删除单例的唯一实例。

使用这种方法释放单例对象有以下特征:

  • 在单例类内部定义专有的嵌套类。
  • 在单例类内定义私有的专门用于释放的静态成员。
  • 利用程序在结束时析构全局变量的特性,选择最终的释放时机。

上述代码不仅麻烦,并且在多线程环境下会出现竞争,考虑使用双检测锁模式(Double-Checked locking Pattern)

加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <mutex>
using namespace std;
std::mutex mt;
class Singleton
{
private:
Singleton(){}
public:
static Singleton* instance()
{
mt.lock(); // 加锁
if(_instance == 0)
_instance = new Singleton();
mt.unlock(); // 解锁
return _instance;
}
private:
static Singleton* _instance;
};
Singleton* Singleton::_instance = 0;

上锁后是解决了线程安全问题,但是有些资源浪费。稍微分析一下:每次instance函数调用时候都需要请求加锁,其实并不需要,instance函数只需第一次调用的时候上锁就行了。这时可以用DCLP解决。

双检查锁,由于内存读写导致不安全

Double-Checked Locking Pattern

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
#include <iostream>
#include <mutex>
using namespace std;
std::mutex mt;
class Singleton
{
private:
Singleton(){}
public:
static Singleton* instance()
{
if(_instance == 0)
{
mt.lock();
if(_instance == 0)
_instance = new Singleton();
mt.unlock();
}
return _instance;
}
private:
static Singleton* _instance;
public:
int atestvalue;
};
Singleton* Singleton::_instance = 0;

这个版本很不错,又叫“双重检查”Double-Check。下面是说明:

  1. 第一个条件是说,如果实例创建了,那就不需要同步了,直接返回就好了。
  2. 不然,我们就开始同步线程。
  3. 第二个条件是说,如果被同步的线程中,有一个线程创建了对象,那么别的线程就不用再创建了。

分析

1
_instance = new Singleton();

为了执行这句代码,机器需要做三样事儿:

  1. singleton对象分配空间。
  2. 在分配的空间中构造对象
  3. 使_instance指向分配的空间

遗憾的是编译器并不是严格按照上面的顺序来执行的。可以交换2和3.

将上面三个步骤标记到代码中就是这样:

1
2
3
4
5
6
7
8
9
10
11
12
Singleton* Singleton::instance() {
if (_instance == 0) {
mt.lock();
if (_instance == 0) {
_instance = // Step 3
operator new(sizeof(Singleton)); // Step 1
new (_instance) Singleton; // Step 2
}
mt.unlock();
}
return _instance;
}
  • 线程A进入了instance函数,并且执行了step1和step3,然后挂起。这时的状态是:_instance不 NULL,而_instance指向的内存区没有对象!
  • 线程B进入了instance函数,发现_instance不为null,就直接return _instance了。

local static

C++ 11 规定了 local static 在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
private:
Singleton() { };
~Singleton() { };
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() // 返回引用
{
static Singleton instance; // 静态局部变量
return instance;
}
};
  1. gcc 4.0之后的编译器支持这种写法。
  2. C++11及以后的版本(如C++14)的多线程下,正确。
  3. C++11之前不能这么写。

C++11 call_once

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
39
40
41
42
43
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

std::once_flag flag;

class Singleton
{
public:
static Singleton& getInstance()
{
std::call_once(flag, []() {instance_.reset(new Singleton()); });
return *instance_;
}

private:
static std::unique_ptr<Singleton> instance_;

private:
Singleton() = default;
Singleton(const Singleton& other) = delete;
Singleton& operator=(const Singleton&) = delete;
};

std::unique_ptr<Singleton> Singleton::instance_;

void do_onceflag()
{
Singleton& s = Singleton::getInstance();
cout << &s << endl;
}

int main()
{
std::thread t1(do_onceflag);
std::thread t2(do_onceflag);

t1.join();
t2.join();

return 0;
}

饿汉版

单例实例在程序运行时被立即执行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
private:
static Singleton instance;
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton& getInstance() {
return instance;
}
};
Singleton Singleton::instance;

由于在 main 函数之前初始化,所以没有线程安全的问题。但是潜在问题在于 no-local static 对象(函数外的 static对象)在不同编译单元中的初始化顺序是未定义的。也即,static Singleton instance;和 static Singleton& getInstance() 二者的初始化顺序不确定,如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。