设计模式——单例模式

设计模式:

设计模式代表了最佳实践,是软件开发过程中面临一般问题的解决方案。 设计模式是一套被反复使用、经过分类、代码设计总结的经验。

单例模式

单例模式也叫单件模式。Singleton是一个非常常用的设计模式,几乎所有稍微大一些的程序都会使用到它,所以构建一个线程安全并且 高效的Singleton很重要。

1. 单例类保证全局只有一个唯一实例对象。

2. 单例类提供获取这个唯一实例的接口。

由于要求只生成一个实例,因此我们必须把构造函数的访问权限标记为protected或private,限制只能在类内创建对象.

单例类要提供一个访问唯一实例的接口函数(全局访问点),就需要在类中定义一个static函数,返回在类内部唯一构造的实例。


 两个概念:

 懒汉模式 (lazy loading ):第一次调用GetInstance才创建实例对象,比较复杂
 饿汉模式:  程序一运行,就创建实例对象、简洁高效 ,但有些场景下不适用 

方法一:不考虑线程安全,只适用于单线程环境的单例类

定义一个静态的实例,在需要的时候创建该实例 (懒汉模式)

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
class Singleton
{
public:
    //获取唯一对象实例的接口函数
    static Singleton* GetInstance()
    {
        if (_instance == NULL)
        {
            _instance = new Singleton();
        }
        return _instance;
    }
    static void DelInstance()
    {
        if (_instance != NULL)
        {
            delete _instance;
            _instance = NULL;
        }
    }
    void Print()
    {
        cout << _data << endl;
    }
protected:
    //构造函数标记为protected或private,限制只能在类内创建对象
    Singleton()
        :_data(5)
    {}
 
    //防拷贝
    Singleton(const Singleton&);
    Singleton operator=(const Singleton&);
private:       
    //指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
    static Singleton* _instance;      // 单实例对象
    int _data;  //单实例对象中的数据
};
// 静态成员在类外初始化
Singleton* Singleton::_instance = NULL;

  这种方法是最简单、最普遍的方法。只有在_instance为NULL的时候才会创建一个实例以避免重复创建。同时我们把构造函数定义为私有函数,这样就能确保只创建一个实例。

但是上述的代码在单线程的时候工作正常,在多线程的情况下就有问题了。

  设想如果两个线程同时运行到判断_instance是否为NULL的 if 语句那里,并且_instance之前并未创建时,这两个线程各自就都会创建一实例,这是就无法满足单例模式的要求了。


 方法二:能在多线程环境下工作,但是效率不高

为了保障在多线程环境下只得到一个实例,需要加一把互斥锁。把上述代码稍作修改,即:

ps: 下面部分的加锁使用了C++11库的互斥锁

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
44
45
46
47
48
49
class Singleton
{
public:
    //获取唯一对象实例的接口函数
    static Singleton* GetInstance()
    {
        //lock();        //C++中没有直接的lock()
        //RAII
        //lock lk;
        _sMtx.lock();  //C++11
        if (_instance == NULL)
        {
            _instance = new Singleton();
        }
        //unlock();
        _sMtx.unlock();
        return _instance;
    }
    static void DelInstance()
    {
        if (_instance != NULL)
        {
            delete _instance;
            _instance = NULL;
        }
    }
    void Print()
    {
        cout << _data << endl;
    }
protected:
    //构造函数标记为protected或private,限制只能在类内创建对象
    Singleton()
        :_data(5)
    {}
 
    //防拷贝
    Singleton(const Singleton&);
    Singleton operator=(const Singleton&);
 
private:
    //指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
    static Singleton* _instance;      // 单实例对象
    int _data;                                // 单实例对象中的数据
    static mutex _sMtx;              // 互斥锁
};
// 静态成员在类外初始化
Singleton* Singleton::_instance = NULL;
mutex Singleton::_sMtx;

  设想有两个线程同时想创建一个实例,由于在一个时刻,只有一个线程能得到互斥锁,所以当第一个线程加上锁后,第二个线程就只能等待。当第一个线程发现实例还没有创建时,它就建立一个实例。接着第一个线程释放锁,此时第二个线程进入并上锁,这个时候由于实例已经被第一个线程创建出来了,第二个线程就不会重复创建实例了,这样就保证在多线程环境下只能得到一个实例。

  但是,每次获取唯一实例,程序都会加锁,而加锁是一个非常耗时的操作,在没有必要的时候,我们要尽量避免,否则会影响性能。


 方法三:使用双重检查,提高效率,避免高并发场景下每次获取实例对象都进行加锁,并使用内存栅栏防止重排

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
44
45
46
47
48
49
50
51
52
53
54
class Singleton
{
public:
    //获取唯一对象实例的接口函数
    static Singleton* GetInstance()
    {
        // 使用双重检查,提高效率,避免高并发场景下每次获取实例对象都进行加锁
        if (_instance == NULL)
        {
            std::lock_guard lck(_sMtx);
            if (_instance == NULL)
            {
                // tmp = new Singleton()分为以下三个部分
                // 1.分配空间2.调用构造函数3.赋值
                // 编译器编译优化可能会把2和3进行指令重排,这样可能会导致高并发场景下,其他线程获取到未调用构造函数初始化的对象
                // 以下加入内存栅栏进行处理,防止编译器重排栅栏后面的赋值到内存栅栏之前
                Singleton* tmp = new Singleton();
                MemoryBarrier();
                _instance = tmp;
            }
        }
        return _instance;
    }
    static void DelInstance()
    {
        if (_instance != NULL)
        {
            delete _instance;
            _instance = NULL;
        }
    }
    void Print()
    {
        cout << _data << endl;
    }
protected:
    //构造函数标记为protected或private,限制只能在类内创建对象
    Singleton()
        :_data(5)
    {}
 
    //防拷贝
    Singleton(const Singleton&);
    Singleton operator=(const Singleton&);
 
private:
    //指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
    static Singleton* _instance;      // 单实例对象
    int _data;                                // 单实例对象中的数据
    static mutex _sMtx;              // 互斥锁
};
// 静态成员在类外初始化
Singleton* Singleton::_instance = NULL;
mutex Singleton::_sMtx;

  试想,当实例还未创建时,由于 Singleton == NULL ,所以很明显,两个线程都可以通过第一重的 if 判断 ,进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 if 判断 ,而另外的一个线程则会在 lock 语句的外面等待。而当第一个线程执行完 new  Singleton()语句退出锁定区域,第二个线程便可以进入 lock 语句块,此时,如果没有第二重Singleton == NULL的话,那么第二个线程还是可以调用 new  Singleton()语句,第二个线程仍旧会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定(第二层if 判断必须存在)。

   多数现代计算机为了提高性能而采取乱序执行,这使得内存栅栏成为必须。barrier就象是代码中的一个栅栏,将代码逻辑分成两段,barrier之前的代码和barrier之后的代码在经过编译器编译后顺序不能乱掉。也就是说,barrier之后的代码对应的汇编,不能跑到barrier之前去,反之亦然。之所以这么做是因为在我们这个场景中,如果编译器为了榨取CPU的performace而对汇编指令进行重排,其它线程获取到未调用构造函数初始化的对象,很有可能导致出错。

   只有第一次调用_instance为NULL,并且试图创建实例的时候才需要加锁,当_instance已经创建出来后,则没必要加锁。这样的修改比之前的时间效率要好很多。

但是这样的实现比较复杂,容易出错,我们还可以利用饿汉模式,创建相对简洁高效的单例模式。


方法四:饿汉模式--简洁、高效、不用加锁、但是在某些场景下会有缺陷

  因为静态成员的初始化在程序开始时,也就是进入主函数之前,由主线程以单线程方式完成了初始化,所以静态初始化实例保证了线程安全性。在性能要求比较高时,就可以使用这种方式,从而避免频繁的加锁和解锁造成的资源浪费。

?
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
class Singleton
{
public:
    //获取唯一对象实例的接口函数
    static Singleton* GetInstance()
    {
        assert(_instance);
        return _instance;
    }
    void Print()
    {
        cout << _data << endl;
    }
protected:
    //构造函数标记为protected或private,限制只能在类内创建对象
    Singleton()
        :_data(5)
    {}
 
    //防拷贝
    Singleton(const Singleton&);
    Singleton operator=(const Singleton&);
 
private:
    static Singleton* _instance;      // 单实例对象
    int _data;          // 单实例对象中的数据
};
Singleton* Singleton::_instance = new Singleton;

 代码实现非常简洁。创建的实例_instance并不是在第一次调用GetInstance接口函数时才创建,而是在初始化静态变量的时候就创建一个实例。如果按照该方法会过早的创建实例,从而降低内存的使用效率。 

方法五:方法四还可以再简化点

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
class Singleton
{
public:
    //获取唯一对象实例的接口函数
    static Singleton* GetInstance()
    {
        static Singleton instance;
        return &instance;
    }
    void Print()
    {
        cout << _data << endl;
    }
protected:
    //构造函数标记为protected或private,限制只能在类内创建对象
    Singleton()
        :_data(5)
    {}
 
    //防拷贝
    Singleton(const Singleton&);
    Singleton operator=(const Singleton&);
 
private:
    int _data;  // 单实例对象中的数据
};

 实例销毁

 此处使用了一个内部GC类,而该类的作用就是用来释放资源

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
44
45
46
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//带RAII GC自动回收实例对象的方式
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 
class Singleton
{
public:
    // 获取唯一对象实例的接口函数
    static Singleton* GetInstance()
    {
        assert(_instance);
        return _instance;
    }
    // 删除实例对象
    static void DelInstance()
    {
        if (_instance)
        {
            delete _instance;
            _instance = NULL;
        }
    }
    void Print()
    {
        cout << _data << endl;
    }
    class GC
    {
    public:
        ~GC()
        {
            cout << "DelInstance()" << endl;
            DelInstance();
        }
    };
private:
    Singleton()
        :_data(5)
    {}
    static Singleton*_instance;
    int _data;
};
// 静态对象在main函数之前初始化,这时只有主线程运行,所以是线程安全的。
Singleton* Singleton::_instance = new Singleton;
// 使用RAII,定义全局的GC对象释放对象实例
Singleton::GC gc;

    在程序运行结束时,系统会调用Singleton中GC的析构函数,该析构函数会进行资源的释放。