27. 单例模式
单例是一种创建型设计模式,保证一个类只有一个实例(对象),并提供一个访问该实例的全局节点。
27.1. 基础单例
1// from the header file
2class Singleton
3{
4public:
5 static Singleton* instance();
6 // something else ...
7private:
8 static Singleton* pInstance;
9};
10
11// from the implementation file
12Singleton* Singleton::pInstance = 0; // nullptr
13
14Singleton* Singleton::instance()
15{
16 if(pInstance == 0)
17 {
18 pInstance = new Singleton;
19 }
20 return pInstance;
21}
这种实现方法不是线程安全的(Thread-safe),多个线程同时调用 instance()
可能会构造出多个对象。
27.2. 全加锁
1Singleton* Singleton::instance()
2{
3 Lock lock; // acquire lock (params omitted for simplicity)
4 if(pInstance == 0)
5 {
6 pInstance = new Singleton;
7 }
8 return pInstance;
9} // release lock (via Lock destructor)
所有线程调用 instance()
都会先加锁,如果加锁不成功,则该线程会阻塞直到加锁成功。因此,可以保证只有一个实例。
缺点是:每一次调用 instance()
都需要加锁,开销很大,尽管实际上只有在第一次调用的时候有加锁的必要。
27.3. DCLP
DCLP(Double-Checked Locking Pattern)避免了重复加锁,只需要在第一次调用的时候加锁。
1Singleton* Singleton::instance()
2{
3 if(pInstance == 0) // 1st test
4 {
5 Lock lock;
6 if(pInstance == 0) // 2nd test
7 {
8 pInstance = new Singleton;
9 }
10 }
11 return pInstance;
12}
执行顺序
pInstance = new Singleton
需要完成三件事情:
step-1:分配内存给即将构造的实例。
step-2:在分配的内存上构造 Singleton 实例。
step-3:指针 pInstance 指向分配的内存。
事实上,由于编译器的优化,这三个步骤并不一定是按照上述顺序完成的,也许 step-3 会在 step-2 之前完成, 这就导致指针 pInstance 在 实例构造之前 已经是非空指针了,另一个线程判断非空之后,可能会去解引用/访问该实例,会导致出错。因此,这不是线程安全的。
volatile
可以尝试使用关键字 volatile
:
static volatile Singleton* volatile instance();
static Singleton* volatile pInstance;
C/C++中的 volatile 和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier。
The C++ Programming Language:
A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.
volatile
提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据,从而可以提供对特殊地址的稳定访问。如果没有 volatile
关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。 volatile
可以保证指令执行的顺序。
但是使用 volatile
仍然面临两个问题:
可以保证单线程内读写数据的顺序,但是不能保证跨线程的读写顺序。
一个实例只有当构造完成、退出构造函数时才会赋予
volatile
属性,因而分配内存和实例初始化的顺序不能保证。
缓存一致性
在多处理器的机器上,DCLP 还面临缓存一致性问题(Cache Coherency Problem):一个处理器上的线程正在创建实例,而另一个处理器上的线程可能会访问到未初始化的实例。
如果一个 CPU 缓存了某块内存,那么在其他 CPU 修改这块内存的时候,希望得到通知。拥有多组缓存的时候,需要它们保持同步,但是,系统的内存在各个 CPU 之间无法做到与生俱来的同步。
结论
推荐使用全加锁方式。为了避免多线程重复加锁,可以缓存指向该实例的指针,即用:
Singleton* const instance = Singleton::instance(); // cache instance pointer
instance->transmogrify();
instance->metamorphose();
instance->transmute();
代替:
Singleton::instance()->transmogrify();
Singleton::instance()->metamorphose();
Singleton::instance()->transmute();
27.4. 另一种实现
下面这种实现是线程安全的。
1class S
2{
3public:
4 static S& getInstance()
5 {
6 static S instance; // Guaranteed to be destroyed.
7 // Instantiated on first use.
8 return instance;
9 }
10private:
11 S() {} // Constructor? (the {} brackets) are needed here.
12
13 // C++ 03
14 // ========
15 // Don't forget to declare these two. You want to make sure they
16 // are inaccessible(especially from outside), otherwise, you may accidentally get copies of
17 // your singleton appearing.
18 S(S const&); // Don't Implement
19 S& operator=(S const&); // Don't implement
20
21 // C++ 11
22 // =======
23 // We can use the better technique of deleting the methods
24 // we don't want.
25public:
26 S(S const&) = delete;
27 S& operator=(S const&) = delete;
28
29 // Note: Scott Meyers mentions in his Effective Modern
30 // C++ book, that deleted functions should generally
31 // be public as it results in better error messages
32 // due to the compilers behavior to check accessibility
33 // before deleted status
34};
1class S
2{
3public:
4 static S& getInstance(int _x)
5 {
6 static S instance(_x);
7 return instance;
8 }
9 S(const S&) = delete;
10 S& operator=(const S&) = delete;
11 int x;
12private:
13 S(int _x): x(_x){}
14};
15
16int main()
17{
18 const S* ps = &S::getInstance(5);
19 cout << ps << " " << ps->x << endl; // 0x6013e0 5
20 const S* pss = &S::getInstance(6);
21 cout << pss << " " << pss->x << endl; // 0x6013e0 5
22}
Note
拷贝构造函数和拷贝赋值运算符需要声明为不可调用;无参构造函数、有参构造函数应该声明为 private。
27.5. 饿汉与懒汉模式
第一节和第四节都是“懒汉”模式(Lazy Mode)的例子:第一次使用到类实例的时候才创建。
“饿汉”模式(Hungry Mode):在使用之前已经创建好了实例,取之即用。
1class Singleton
2{
3public:
4 static Singleton* getInstance()
5 {
6 return p;
7 }
8private:
9 static Singleton* p;
10 Singleton(){}
11};
12
13Singleton* Singleton::p = new Singleton();
“饿汉”模式是线程安全的,因为在进入 main 函数之前就由单线程方式进行了实例化。
Note
上面例子中,静态成员指针初始化调用了私有构造函数。创建普通实例是不能直接调用私有构造函数的。
27.6. 参考资料
C++ and the Perils of Double-Checked Locking
C++ Singleton design pattern
C++ 单例模式讲解和代码示例