跳转至

单例模式

何为单例模式

单例模式是设计模式的一种。提供唯一的类的实例化对象,具有全局变量的特点。任何位置都可以通过接口获得这个实例。

具体应用场景: 1. 设备管理器:有多个设备,只有一个设备管理器,用于管理驱动; 2. 数据池:缓存数据的数据结构,需要在一处写,多处读取或者是多处写多处读取。

单例模式实现

特点

  • static:全局只有一个实例,构造函数设置为private
  • 线程安全
  • 禁止赋值和拷贝
  • 使用static成员函数获取static成员

懒汉式

singleton.cpp
#include <iostream>
using namespace std;

/*
 * version_1:
 * 存在的问题:
 *  线程安全;
 *  内存泄漏
 * */

class Singleton{
    private:
        Singleton() {
            cout<<"constructor called."<<endl;
        } //构造函数设置为私有属性

        Singleton(Singleton&) = delete; //禁止拷贝构造
        Singleton& operator= (const Singleton&) = delete; //禁止赋值

        //静态成员:唯一实例
        static Singleton* m_instance_ptr;

    public:
        ~Singleton() {
            cout<<"deconstructor called"<<endl;
        }

        static Singleton* get_instance() {
            if(m_instance_ptr == nullptr) {
                m_instance_ptr = new Singleton;
            }
            return m_instance_ptr;
        } //静态成员函数获取唯一实例
};


//静态成员类外初始化
Singleton* Singleton::m_instance_ptr = nullptr;


int main() {
    Singleton* singleton_1 = Singleton::get_instance(); //获取第一个实例
    Singleton* singleton_2 = Singleton::get_instance(); //获取第二个实例

    return 0;
}
运行结果如下:
constructor called.

存在问题:

  1. 线程安全:假设现在有两个线程分别都在获取这个实例化对象,第一个线程在进入函数体get_instance()之后判断m_instance_ptr为空,于是创建实例化对象;第二个线程在第一个线程创建对象(还未创建完成)的同时也进入get_instance()函数体,同时也判断m_instance_ptr为空,于是也创建实例化对象;
  2. 内存泄漏:从运行结果中来看,只调用了一次实例化构造,没有调用析构函数;只负责new对象,没有释放对象对应的内存

问题解决:

  1. 使用双检锁:在进入函数get_instance()之后,判断m_instance_ptr是否为空,再使用lock_guard加锁,之后再判断m_instance_ptr是否为空,再选择创建实例化对象。使用双检锁的好处是:只有在m_instance_ptr为空的时候才会加锁,省去不必要的加锁开销;使用lock_guard的好处是:在出作用域之后会自动解锁;
  2. 使用智能指针shared_ptr管理唯一的静态对象

鉴于懒汉式简单直接的做法所带来的问题以及其解决方案,得到以下version2,线程安全、无内存泄漏的的懒汉式单例模式。

线程安全、无内存泄漏的懒汉式

singleton_version2.cpp
#include <iostream>
#include <memory> //shared_ptr
#include <mutex> //锁
using namespace std;

/*
 * version_1:
 * 存在的问题:
 *  线程安全;
 *  内存泄漏
 * */

/*
 * version_2:
 * 解决问题:
 *  线程安全:双检锁 lock_guard
 *  内存泄漏:智能指针 shared_ptr
 * */

class Singleton{
    private:
        Singleton() {
            cout<<"constructor called."<<endl;
        } //构造函数设置为私有属性

        Singleton(Singleton&) = delete; //禁止拷贝构造
        Singleton& operator= (const Singleton&) = delete; //禁止赋值

        //静态成员:唯一实例
        //shared_ptr管理对象
        static shared_ptr<Singleton> m_instance_ptr;

        //具有全局变量特性的静态互斥量
        static mutex m_mutex; 
    public:
        ~Singleton() {
            cout<<"deconstructor called"<<endl;
        }

        static shared_ptr<Singleton> get_instance() { //返回智能指针对象


            //双检锁:
            if(m_instance_ptr == nullptr) {
                lock_guard<mutex> lk(m_mutex); //加锁(出作用域自动解锁)
                if(m_instance_ptr == nullptr) {
                    m_instance_ptr = shared_ptr<Singleton>(new Singleton);
                }
            }
            return m_instance_ptr;
        } //静态成员函数获取唯一实例
};


//静态成员类外初始化
shared_ptr<Singleton> Singleton::m_instance_ptr = nullptr;
mutex Singleton::m_mutex;


int main() {
    shared_ptr<Singleton> singleton_1 = Singleton::get_instance(); //获取第一个实例
    shared_ptr<Singleton> singleton_2 = Singleton::get_instance(); //获取第二个实例

    return 0;
}
运行结果如下:
constructor called.
deconstructor called

存在问题:

  1. 代码中使用智能指针,用户也需要使用智能指针
  2. 开销较大,代码量较多;
  3. 双检锁有可能失效:在不同平台和编译器的情况下,语句的执行顺序可能并不完全按照代码的语句顺序执行。原来是先构造对象再进行赋值,在编译器执行顺序下有可能就是先赋值再构造对象,这样还是会带来线程安全问题。线程A进入函数体判断m_instance_ptr为空之后,加锁再判断指针是否为空之后构建对象,此时如果发生执行顺序的调换,编译器先赋值再构建对象就会造成此时的对象还是空的,而线程B进入函数体之后经过判断同样还是会创建对象

最终版本:返回局部变量的懒汉式

singleton_version3.cpp
#include <iostream>
using namespace std;

/*
 *version_3: return static local var
 * */

class Singleton {
    private:
        Singleton() {
            cout<<"constructor called."<<endl;
        }

    public:
        ~Singleton() {
            cout<<"deconstructor called."<<endl;
        }
        //delete copy constructor
        Singleton(Singleton&) = delete;
        //delete operator=
        Singleton& operator=(const Singleton&) = delete;

        //return static local var by ref
        static Singleton& get() {
            static Singleton instance_ptr;
            return instance_ptr;
        }
};

int main() {
    Singleton& instance_1 = Singleton::get(); //use ref to get the obj
    Singleton& instance_2 = Singleton::get();

    return 0;
}
运行结果如下:
constructor called.
deconstructor called.
以上方法又称为“Mayer‘s Singleton”。好处是:保证了并发中获取的局部静态变量一定是经过初始化的

不建议返回指针

return by pointer
        static Singleton* get() {
            static Singleton instance_ptr;
            return &instance_ptr;
        }
原因是:无法避免用户使用delete instance使对象提前销毁

单例模板

实现一次单例模式代码就可以一直复用。

特点:

  1. 基类(单例模式)设置为模板;
  2. 基类的构造函数设置为protected
  3. 子类继承基类,基类以子类类型为 T
  4. 子类将基类设置为友元类,以便于访问基类的构造函数(与 2 对应)
  5. 子类的构造函数设置为private
CRTP
#include <iostream>
using namespace std;

template<typename T>
class Singleton {
    public:
        static T& get_instance() {
            static T instance;
            return instance;
        }
        virtual ~Singleton() {
            cout<<"deconstructor called."<<endl;
        }
        Singleton(Singleton&) = delete;
        Singleton& operator= (const Singleton&) = delete;
    protected:
    //将基类的构造函数设置为protected,便于子类访问
        Singleton() {
            cout<<"constructor called."<<endl;
        }
};

class Derived:public Singleton<Derived> {
    //设置基类为友元类,才可以调用基类的构造函数
    friend class Singleton<Derived>;
    public:
        //delete copy constructor and operator=
        Derived(Derived&) = delete;
        Derived& operator=(const Derived&) = delete;
    private:
        Derived() = default; //将子类的构造函数设置为private,用户不可实例化对象
};

int main() {
    Derived& instance_1 = Derived::get_instance();
    Derived& instance_2 = Derived::get_instance();

    return 0;
}

另一种单例模板

特点

  1. 在基类中设置一个helper struct(空类)名为token,用于构建基类的局部静态变量
  2. 子类可以将构造函数设置为public,前提是只允许Derived(token)构造 这样就不需要将基类在子类中设置为友元类
don't need to declare base class as friend class
#include <iostream>
using namespace std;

template<typename T>
class Singleton{
    public:
    //noexcept()传入的参数必须是不跑出异常的可构造类型
    //构造过程不抛出异常
        static T& get_instance() noexcept(is_nothrow_constructible<T>::value) {
            static T instance{token()};
            //这里相当于:
            /*
            token t = token(); //构造token对象,并以此构造T对象
            static T instance(t);
            */
            return instance;
        }
        virtual ~Singleton() = default;
        Singleton(Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;

    protected:
        struct token{}; //helper class:empty
        Singleton() noexcept = default;
};

class Derived:public Singleton<Derived> {
    public:
    //子类只允许此构造函数:需要传递结构体token才可以构造子类,这里必须设置为public  
        Derived(token) {
            cout<<"constructor called."<<endl;
        }
        ~Derived() {
            cout<<"deconstructor called."<<endl;
        }

        Derived(Derived&) = delete;
        Derived& operator=(const Derived&) = delete;
};

int main() {
    Derived& instance_1 = Derived::get_instance();
    Derived& instance_2 = Derived::get_instance();

    return 0;
}

补充

  • std::is_nothrow_constructible:用于检查给定类型T是否为是带参数集的可构造类型
  • std::is_nothrow_constructible::value:
    • 1 -- true:类型T可以从Args构造
    • 0 -- false:类型T无法从Args构造

测试程序

test.cpp
// is_nothrow_constructible example
#include <iostream>
#include <type_traits>

struct A { };
struct B {
  B(){}
  B(const A&) noexcept {}
};

int main() {
  std::cout << std::boolalpha; //将 1 和 0 转换为 true 和 false
  std::cout << "is_nothrow_constructible:" << std::endl;
  std::cout << "int(): " << std::is_nothrow_constructible<int>::value << std::endl;
  std::cout << "A(): " << std::is_nothrow_constructible<A>::value << std::endl;
  std::cout << "B(): " << std::is_nothrow_constructible<B>::value << std::endl;
  std::cout << "B(A): " << std::is_nothrow_constructible<B,A>::value << std::endl;
  return 0;
}

输出:

is_nothrow_constructible:
int(): true
A(): true
B(): false
B(A): true

将上述B的构造函数设置为不跑出异常的

将B构造函数设置为不跑出异常的
// is_nothrow_constructible example
#include <iostream>
#include <type_traits>

struct A { };
struct B {
  B() noexcept {}
  B(const A&) noexcept {}
};

int main() {
  std::cout << std::boolalpha;
  std::cout << "is_nothrow_constructible:" << std::endl;
  std::cout << "int(): " << std::is_nothrow_constructible<int>::value << std::endl;
  std::cout << "A(): " << std::is_nothrow_constructible<A>::value << std::endl;
  std::cout << "B(): " << std::is_nothrow_constructible<B>::value << std::endl;
  std::cout << "B(A): " << std::is_nothrow_constructible<B,A>::value << std::endl;
  return 0;
}

输出:

is_nothrow_constructible:
int(): true
A(): true
B(): true
B(A): true

使用pthread保证的单例

来自muduo库的单例模式:

#include "muduo/base/noncopyable.h"

#include <assert.h>
#include <pthread.h>
#include <stdlib.h> // atexit

namespace muduo
{

namespace detail
{
// This doesn't detect inherited member functions!
// http://stackoverflow.com/questions/1966362/sfinae-to-check-for-inherited-member-functions
template<typename T>
struct has_no_destroy //检查该类是否具有这个函数
{
  template <typename C> static char test(decltype(&C::no_destroy)); //传入参数为C类的no_destroy函数地址
  template <typename C> static int32_t test(...);
  const static bool value = sizeof(test<T>(0)) == 1; //true or false 判断这个该类的test函数是否存在
};
}  // namespace detail

template<typename T>
class Singleton : noncopyable //noncopyable:不可复制
{
 public:
  Singleton() = delete; //构造函数删除
  ~Singleton() = delete;

  static T& instance() //提供单例的静态成员函数
  {
    pthread_once(&ponce_, &Singleton::init); //使用pthread_once保证生成单例的唯一和安全
    assert(value_ != NULL);
    return *value_;
  }

 private:
  static void init()
  {
    value_ = new T();
    if (!detail::has_no_destroy<T>::value) //判断T类型有没有no_destroy这个函数
    {
      ::atexit(destroy); //没有的话则退出
    }
  }

  static void destroy()
  {
    typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1]; //T是否为一个完整的类型
    T_must_be_complete_type dummy; (void) dummy; //完整类型则可以执行这条语句

    delete value_;
    value_ = NULL;
  }

 private:
  static pthread_once_t ponce_;
  static T*             value_; //生成单例类的指针
};

template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT; //pthread_once初始化

template<typename T>
T* Singleton<T>::value_ = NULL;

}  // namespace muduo
  • 构造函数删除
  • 继承属性noncopyable表示该模板类不可复制/赋值
  • 维护静态成员变量:T类型指针用于生成唯一安全的单例;ponce用于在POSIX层面保证生成(初始化)单例的过程的线程安全的
  • 返回静态单例的成员函数:调用pthread_once传入singleton的初始化函数(初始化T类型),保证生成单例对象的过程是线程安全的
  • 初始化函数:new一个对象,确保T类型实现了自己的no_destroy函数
  • destroy:判断T此时是否为一个完整的类型,是则执行delete





总结

  1. 单例模式特点:类具有一个唯一的static成员,能通过static成员函数获取这个成员;
  2. 线程安全、无内存泄漏的懒汉式单例模式:在类中提供一个static成员函数在双检锁的保护下创建类的对象(new,使用智能指针管理对象)并返回
  3. 最推荐懒汉式单例模式:在类中提供一个static成员函数,在此成员函数中生成static局部变量并返回
  4. 一般的单例模板:
    • 基类:(模板)
      • 禁止赋值和拷贝
      • 构造函数设置为protected
      • virtual析构函数
      • static成员函数返回static局部变量
    • 子类:public继承子类类型的基类
      • friend声明基类
      • 构造函数设置为private
      • 禁止赋值和拷贝
  5. 另一种单例模式:
    • 基类:(模板)
      • 禁止赋值和拷贝
      • protected的struct token {} 用于辅助构造
      • static T& get_instance() noexcept(std::is_nothrow_constructible<T>::value)
        函数体实现:
        static T instance{token()};
        return instance;
    • 子类:public继承子类类型的基类
      • 只允许Derived(token)构造,可以将其设置为public
  6. std::is_nothrow_constructible<T, Args...>用于检查类型T是否可以由参数表Args得出
    • std::is_nothrow_constructible<T, Args...>::value
      • 1:true_type
      • 0:false_type