单例模式

在面向对象编程中,有时候我们希望达到这样一种效果,一个类只有一个实例化的对象,比如线程池,缓存等,所以人们人为规定,这些类有且只有一个唯一的实例。这种设计模式被称为单例模式。

 

单例模式的特点

  • 使用单例模式的类没有公开的构造函数,所以不能创建该类的实例
  • 同理,使用单例模式的类也没有公开的拷贝函数和赋值函数
  • 使用单例模式的类需要提供一个公有方法,让外部能访问到这个唯一的实例

这样,我们可以大致写出这个类的大致结构,它的构造函数,赋值函数和拷贝函数都是私有的,只有获取实例的接口是公有的。唯一的实例要使用static关键字标记,以保证唯一性。

 1 class Singleton{
 2 
 3 public:
 4     Singleton*getInstance();
 5 private:
 6     Singleton();
 7     Singleton(const Singleton&);
 8     Singleton& operator =(const Singleton&);
 9     static Singleton*_instance;
10 };

 

 

单例模式分类

单例模式有两种主要的实现方法,懒汉模式和饿汉模式。

1. 懒汉模式

懒汉模式的特点是当外界调用时才进行实例化。下面是常见的一种懒汉模式的写法

1 Singleton*Singleton::getInstance()
2 {
3     if (_instance == nullptr)
4         _instance = new Singleton;
5     return _instance;
6 }

懒汉模式的线程安全问题

上面这种写法在单线程模式下是没有问题的,但多线程环境下是不安全的,假如这个唯一的实例还没有创建,这时有两个线程同时调用GetInstance方法,有可能会发生下面这种情况:

 

这种情况下,线程A,B可能会创建两个不同的对象,导致程序错误。 

解决这个问题的一个很自然的想法是对它加锁。

1 mutex m;
2 Singleton*Singleton::getInstance(){
3     m.lock();
4     if (_instance == nullptr)
5         _instance = new Singleton;
6     m.unlock();
7     return _instance;
8 }

 

但是加锁又会带来另外的性能问题,如果每个线程每次获取实例都加锁,有可能造成阻塞的发生。实际上,上锁的目的是为了防止有多个线程在实例未被初始化的情况下,同时对他进行初始化,如果实例已经被创建了,就不需要考虑这个问题了,所以就可以采用二次加锁的方法来提高程序的性能。

二次加锁检查

 1 Singleton*Singleton::getInstance(){
 2     
 3     if (_instance == nullptr)
 4     {
 5         m.lock();
 6         if (_instance == nullptr)
 7         {
 8             _instance = new Singleton;
 9         }
10         m.unlock();
11     }
12     return _instance;
13 }

二次加锁检测的内存乱序问题

虽然二次计算加锁检测可以避免阻塞,但是可能会造成内存乱序问题,究其原因还是出在

_instance = new Singleton

这个语句上,这个语句分三个步骤执行

1. 分配Singleton类型对象所需的内存

2. 在分配的内存出构造Singleton对象

3. 将分配的内存的地址赋给指针_instance

其中2,3两个步骤的顺序是不一定的,这就可能出现一种情况,即_instance已经得到了地址,但是Singleton对象却还没有构造出来。这就可能出现严重的bugger。

 

推荐的懒汉模式写法

Soctt Meyers 在 《Effiective C++》中提出了一种高效便捷的懒汉模式实现方法。这种方法直接在getInstance中定义了一种static的Singleton变量并返回

class Singleton{

public:
    Singleton*getInstance();
private:
    Singleton();
    Singleton(const Singleton&);
    Singleton& operator =(const Singleton&);
    //static Singleton*_instance;
};
Singleton*Singleton::getInstance(){

    static Singleton local_instance;
    return &local_instance;
}

注意,该模式在C++11前可能发生错误。

2. 饿汉模式

饿汉模式的特点是一开始就对实例进行初始化,调用时直接返回这个构建好的实例。

class Singleton{

public:
    Singleton*getInstance();
private:
    Singleton();
    Singleton(const Singleton&);
    Singleton& operator =(const Singleton&);
    static Singleton _instance;
};
Singleton*Singleton::getInstance(){

    return &_instance;
}

饿汉模式不会面临线程安全的问题,因为实例从一开始就已经被创建好了。