菜鸟笔记
提升您的技术认知

用C++写一个单例模式

“请用C++写一个单例,考虑一下多线程环境。”
这是一个常见的面试题,别人问过我,我也问过别人。
这个问题可以很简单,也可以很复杂。

简单有效的单例

class Singleton {
public:
    static Singleton* GetInstance() {
        Singleton singleton;
        return &singleton;
    }
};

在C++11中静态局部变量的初始化是线程安全的,参考链接
这种写法既简单,又是线程安全的,可以满足大多数场景的需求。

饿汉模式

单例在程序初期进行初始化。即如论如何都会初始化。

class Singleton {
public:
    static Singleton* GetInstance() {
        return singleton;
    }   
static Singleton* singleton;
};
Singleton* Singleton::singleton = new Singleton();

这种写法也是线程安全的,不过Singleton的构造函数在main函数之前执行,有些场景下是不允许这么做的。改进一下:

class Singleton {
public:
    static Singleton* GetInstance() {
        return singleton;
    }   
    int Init();
    static Singleton* singleton;
};
Singleton* Singleton::singleton = new Singleton();

将复杂的初始化操作放在Init函数中,在主线程中调用。

懒汉模式

单例在首次调用时进行初始化。

class Singleton {
public:
    static Singleton* GetInstance() {
        if (singleton == NULL) {
            singleton = new Singleton();
        }
        return singleton;
    }
    static Singleton* singleton;
};
Singleton* Singleton::singleton = NULL;

这样写不是线程安全的。改进一下:

class Singleton {
public:
    static Singleton* GetInstance() {
        lock();
        if (singleton == NULL) {
            singleton = new Singleton();
        }
        unlock();
        return singleton;
    }
    static Singleton* singleton;
};
Singleton* Singleton::singleton = NULL;

这样写虽是线程安全的,但每次都要加锁会影响性能。

DCLP(Double-Checked Locking Pattern)

在懒汉模式的基础上再改进一下:

class Singleton {
public:
    static Singleton* GetInstance() {
        if (singleton == NULL) {
            lock();
            if (singleton == NULL) {
                singleton = new Singleton();
            }
            unlock();
        }
        return singleton;
    }
    static Singleton* singleton;
};
Singleton* Singleton::singleton = NULL;

两次if判断避免了每次都要加锁。但是,这样仍是不安全的。因为”singleton = new Singleton();”这句不是原子的。
这句可以分为3步:

  1. 申请内存
  2. 调用构造函数
  3. 将内存指针赋值给singleton

上面这个顺序是我们期望的,可以编译器并不会保证这个执行顺序。所以也有可能是按下面这个顺序执行的:

  1. 申请内存
  2. 将内存指针赋值给singleton
  3. 调用构造函数

这样就会导致其他线程可能获取到未构造好的单例指针。
解决办法:

class Singleton {
public:
    static Singleton* GetInstance() {
        if (singleton == NULL) {
            lock();
            if (singleton == NULL) {
                Singleton* tmp = new Singleton();
                memory_barrier();  // 内存屏障
                singleton = tmp;
            }
            unlock();
        }
        return singleton;
    }
    static Singleton* singleton;
};
Singleton* Singleton::singleton = NULL;

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。简单的说就是保证指令一定程度上的按顺序执行,避免上述所说的乱序行为。
把单例写成这么复杂也是醉了。

返回指针还是引用?

Singleton返回的实例的生存期是由Singleton本身所决定的,而不是用户代码。我们知道,指针和引用在语法上的最大区别就是指针可以为NULL,并可以通过delete运算符删除指针所指的实例,而引用则不可以。由该语法区别引申出的语义区别之一就是这些实例的生存期意义:通过引用所返回的实例,生存期由非用户代码管理,而通过指针返回的实例,其可能在某个时间点没有被创建,或是可以被删除的。但是这两条Singleton都不满足,所以返回引用更好一些。

结论

class Singleton {
public:
    static Singleton& GetInstance() {
        static Singleton singleton;
        return singleton;
    }
    // 如果需要有比较重的初始化操作,则在安全的情况下初始化
    int Init();
private:
    // 禁用构造函数、拷贝构造函数、拷贝函数
    Singleton();
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
};

这种写法比较简单,可以满足大多数场景的需求。如果不能满足需求,再考虑DCLP那种复杂的模式。如《UNIX编程艺术》中所说:“Keep it sample, Stupid!”