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. 参考资料

  1. C++ and the Perils of Double-Checked Locking

  1. C++ Singleton design pattern

  1. C++ 单例模式讲解和代码示例