单例模式c++实现

单例模式

基本知识

单例模式的学习在多次中遇到,包括但不限于:侯捷:明确拒绝编译器合成构造函数 muduo:Singleton类 flydragon:Config类 因此在这里做一个总结,该如何去用现代C++写一个线程安全的类。

定义:一个类有且仅有一个对象,这种类的设计模式称为单例模式或者单件模式。

用途举例:整个工程读取配置文件的对象,日志对象等。

定义级需求:能够实现只有一个对象。

实现方法:将构造函数,拷贝构造函数,拷贝赋值函数定义为 private; 这样可以避免编译器为我们合成构造函数,也可以避免外界的调用。在C++11中还提供了delete机制可以解决这个问题。

如下:

class Singleton
{
private:
	Singleton();
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
};
//c++11的delete机制,要采取这种做法更现代化  但是delete机制之后没法new了
class Singleton
{
public://delete机制下要放到public
	Singleton() = delete;
	Singleton(const Singleton&) = delete; 
	Singleton& operator=(const Singleton&) = delete;
}

既然我们已经无法通过调用构造函数来生成对象,那么我们该如何去使用这个类呢以及它的独有对象呢?我们采用public & static 方法,定义类的静态成员变量和静态成员函数,用户可以直接通过类名调用静态成员。

//class中定义
public:
	static Singleton* getInstance()
	{
		if (instance_ == NULL)
			instance_ = new Singleton();
		return instance_;
	}
//用户代码中
Singleton* ct1 = Singleton::getInstance();

这样就实现了要求的只能构造一个对象的最简单的饿汉单例

但是这个版本是很明显存在着线程安全问题的:当线程A执行到instance_ == NULL,而还未执行new的时候,线程B也正好执行到instance_ == NULL,这样线程B也就获得了 执行new的机会,因此的话,Singleton就会被初始化两次。

class Singleton
{
private:
	Singleton() { std::cout << "构造单例对象" << std::endl; };
public:
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

	static Singleton *instance_; // 单例对象在这里!
public:
	static Singleton* getInstance()
	{
		if (instance_ == NULL)
			instance_ = new Singleton();
		return instance_;
	}
};

因为现在用了 new 需要delete释放内存,而这很容易造成内存泄露,因此我们肯定是拒绝这种写法,因此考虑采用shared_ptr智能指针。但是shared_ptr只能用于懒汉模式

那么我先对比一下看看懒汉模式:

饿汉模式,就是在类定义的时候就实例化了(因为饿,主观能动性强 - . -)。

Singleton* Singleton:: instance = new Singleton();

是线程安全的,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现(不用锁机制,开销小),可以实现更好的性能。

全局变量、文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量(一般为函数内的静态变量)在第一次使用时分配内存并初始化。这里的变量包含内置数据类型和自定义类型的对象。

  5 class Singleton{
  6     private:
  7         Singleton(){
  8         cout<<"i am single"<<endl;
  9         }
 10 
 11         static Singleton* instance;
 12     public:
 13         static Singleton* getInstance(){
 14             return instance;
 15         }
 16 };
 17 
 18 Singleton* Singleton:: instance = new Singleton();
 19 
 20 int main(){
 21 	Singleton* one = Singleton::getInstance();
 22 	Singleton* two = Singleton::getInstance();
 23 	if(one == two){
 24 		cout<<"确实是单例!"<<endl;
 25 	}
 26 }

饿汉单例的实现

实现的需求:线程安全 且 无内存泄露

1. 是一个"懒汉"单例模式,按需内存分配 也就是 需要用到时才分配对象
2. 基于模板实现,具有很强的通用性。   
3. 自动内存析构,不存在内存泄露问题(使用std::shared_ptr)。
4. 在多线程情况下,是线程安全的。
5. 尽可能的高效。(线程安全必定涉及到线程同步,线程同步分为内核级别和用户级别的同步对象,用户级别效率远高于内核级别的同步对象,而用户级别效率最高的是  InterlockedXXXX系列API)。

因此产生了多个版本

版本一:双检查锁,由于内存读写的乱序导致不安全

上面的做法是不管三七二十一,某个线程要访问的时候,先锁上再说,这样会导致不必要的锁的消耗,那么,是否可以先判断下if (m_instance == nullptr)呢,如果满足,说明根本不需要锁啊!这就是所谓的**双检查锁(DCL)**的思想,DCL即double-checked locking。

//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
    //先判断是不是初始化了,如果初始化过,就再也不会使用锁了
    if(m_instance==nullptr){
        Lock lock; //伪代码
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
    }
    return m_instance;
}

这样看起来很棒!只有在第一次必要的时候才会使用锁,之后就和实现一中一样了。

在相当长的一段时间,迷惑了很多人,在2000年的时候才被人发现漏洞,而且在每种语言上都发现了。原因是内存读写的乱序执行(编译器的问题)。

分析:m_instance = new Singleton()这句话可以分成三个步骤来执行:

  1. 分配了一个Singleton类型对象所需要的内存。
  2. 在分配的内存处构造Singleton类型的对象。
  3. 把分配的内存的地址赋给指针m_instance

可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1是最先执行的,步骤23却不一定。问题就出现在这。假如某个线程A在调用执行m_instance = new Singleton()的时候是按照1,3,2的顺序的,那么刚刚执行完步骤3Singleton类型分配了内存(此时m_instance就不是nullptr了)就切换到了线程B,由于m_instance已经不是nullptr了,所以线程B会直接执行return m_instance得到一个对象,而这个对象并没有真正的被构造!!严重bug就这么发生了。

版本二:C++11的跨平台实现

javac#发现这个问题后,就加了一个关键字volatile,在声明m_instance变量的时候,要加上volatile修饰,编译器看到之后,就知道这个地方不能够reorder(一定要先分配内存,在执行构造器,都完成之后再赋值)。

而对于c++标准却一直没有改正,所以VC++2005版本也加入了这个关键字,但是这并不能够跨平台(只支持微软平台)。

而到了c++ 11版本,终于有了这样的机制帮助我们实现跨平台的方案。

//C++ 11版本之后的跨平台实现 
// atomic c++11中提供的原子操作
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

/*
* std::atomic_thread_fence(std::memory_order_acquire); 
* std::atomic_thread_fence(std::memory_order_release);
* 这两句话可以保证他们之间的语句不会发生乱序执行。
*/
Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);//释放内存fence
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

版本三:pthread_once函数实现

在linux中,pthread_once()函数可以保证某个函数只执行一次

声明: int pthread_once(pthread_once_t once_control, void (init_routine) (void));

功能: 本函数使用初值为PTHREAD_ONCE_INIT的once_control
变量保证init_routine()函数在本进程执行序列中仅执行一次。 
class Singleton{
public:
    static Singleton* getInstance(){
        // init函数只会执行一次
        pthread_once(&ponce_, &Singleton::init);
        return m_instance;
    }
private:
    Singleton(); //私有构造函数,不允许使用者自己生成对象
    Singleton(const Singleton& other);
    //要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)
    static void init() {
        m_instance = new Singleton();
      }
    static pthread_once_t ponce_;
    static Singleton* m_instance; //静态成员变量 
};
pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;
Singleton* Singleton::m_instance=nullptr; //静态成员需要先初始化

版本四:C++11的最简洁跨平台实现

在C++memory model中对static local variable,说道:The initialization of such a variable is defined to occur the first time control passes through its declaration; for multiple threads calling the function, this means there’s the potential for a race condition to define first.

局部静态变量不仅只会初始化一次,而且初始化还是线程安全的,在c++11版本中,如果一个线程正在初始化一个局部静态变量,而另一个线程也试图初始化这个变量,后面的线程会阻塞,而且静态成员变量会在程序结束时自动释放,不需要考虑内存问题

class Singleton{
public:
    // 注意返回的是引用。
    static Singleton& getInstance(){
        static Singleton m_instance;  //局部静态变量
        return m_instance;
    }
private:
    Singleton(); //私有构造函数,不允许使用者自己生成对象
    Singleton(const Singleton& other);
};

这种单例被称为Meyers' Singleton。这种方法很简洁,也很完美,但是注意:

  1. gcc 4.0之后的编译器支持这种写法。
  2. C++11及以后的版本(如C++14)的多线程下,正确。
  3. C++11之前不能这么写。

但是现在都18年了。。新项目一般都支持了c++11了。

用模板包装单例

从上面已经知道了单例模式的各种实现方式。但是有没有感到一点不和谐的地方?如果我class A需要做成单例,需要这么改造class A,如果class B也需要做成单例,还是需要这样改造一番,是不是有点重复劳动的感觉?利用c++的模板语法可以避免这样的重复劳动。

template<typename T>
class Singleton
{
public:
    static T& getInstance() {
        static T value_; //静态局部变量
        return value_;
    }

private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&); //拷贝构造函数
    Singleton& operator=(const Singleton&); // =运算符重载
};

假如有AB两个类,用Singleton类可以很容易的把他们也包装成单例。

class A{
public:
    A(){
       a = 1;
    }
    void func(){
        cout << "A.a = " << a << endl;
    }

private:
    int a;
};

class B{
public:
    B(){
        b = 2;
    }

    void func(){
        cout << "B.b = " << b << endl;
    }
private:
    int b;
};

// 使用demo
int main()
{
    Singleton<A>::getInstance().func();
    Singleton<B>::getInstance().func();
    return 0;
}

饿汉与懒汉的比较

比较:
饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变懒汉式如果在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的推荐使用第一种 。
从实现方式来讲他们最大的区别就是懒汉式是延时加载,他是在需要的时候才创建对象,而饿汉式在虚拟机启动的时候就会创建,饿汉式无需关注多线程问题、写法简单明了、能用则用。但是它是加载类时创建实例(上面有个朋友写错了)、所以如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用一块创建。
懒汉式的优点是延时加载、缺点是应该用同步(想改进的话现在还是不可能,比如double-check)、其实也可以不用同步、看你的需求了,多创建一两个无引用的废对象其实也没什么大不了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值