设计模式之Singleton(单件模式)

1、动机

一、在软件系统中,常常有这样一个特殊的类,必须保证他们在系统中只存在一个实例,才能保证他们的逻辑正确性,以及良好的效率。
二、如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例;
三、这应该是设计者的责任,而不是使用者的责任;
四、保证一个类仅有一个实例,并提供一个该实例的全局访问点(GOF)ios

2、实现分析

一、非线程安全的单件模式;c++

#ifndef SINGLETON_H
#define SINGLETON_H
#include<iostream>
class singleton
{
private:
    singleton(int data=0):data(data)
    { std::cout << "建立构造函数" << std::endl; };
    singleton(const singleton&) {};
    ~singleton() {};
    static singleton* point;
    int data;
public:
    static singleton* get_singleton(int);
    void set_singleton(int);
    void get_data();
};
singleton* singleton::point= nullptr;
singleton* singleton::get_singleton(int data = 0)
{
    if (point == nullptr)
    {
        point = new singleton(data);
    }
    return point;
};
void singleton::set_singleton(int data) 
{
    this->data = data;
}
void singleton::get_data() 
{
    std::cout << "the data is : " << this->data<<std::endl;
}
#endif // !SINGLETON_H

咱们实现了最简单的单例模式,为了知足仅仅生成一个实例,所以咱们应该为客户从新提供一个实例化接口而且屏蔽类的默认构造函数与默认复制构造函数,随后实现这个实例化接口,咱们能够看到如下代码句为核心:安全

singleton* singleton::get_singleton(int data = 0)
{
    if (point == nullptr)
    {
        point = new singleton(data);
    }
    return point;
};

首先判断是否已经实例化了这个单一对象,由于实例化这个单一对象的代价是这个对象的静态成员指针非空,所以可使用这一项进行检查,这个代码在单线程的状况下能够正确使用。
可是在多线程的状况下,若线程1执行了if (point == nullptr)被挂起,而线程2继续执行则会建立一个对象,当线程1再次被激活后又会再建立一个对象,这就形成了以前线程1建立的内存泄漏的问题。
二、单锁的线程安全的单件模式;
因为1中的单件模式存在的问题,使用加锁能够解决这个问题,代码以下:多线程

#ifndef SINGLETON_H
#define SINGLETON_H
#include<iostream>
#include<thread>
#include<mutex>
using std::mutex;
using std::lock_guard;
mutex mut;
class singleton
{
private:
    singleton(int data=0):data(data)
    { std::cout << "建立构造函数" << std::endl; };
    singleton(const singleton&) {};
    ~singleton() {};
    static singleton* point;
    int data;
public:
    static singleton* get_singleton(int);
    void set_singleton(int);
    void get_data();
};
singleton* singleton::point= nullptr;
singleton* singleton::get_singleton(int data = 0)
{
    lock_guard<mutex> lock(mut);//上锁与自动解锁
    if (point == nullptr)
    {
        point = new singleton(data);
    }
    return point;
};
void singleton::set_singleton(int data) 
{
    this->data = data;
}
void singleton::get_data() 
{
    std::cout << "the data is : " << this->data<<std::endl;
}
#endif // !SINGLETON_H

能够看到咱们使用了锁来解决这个问题,这的确在逻辑上达到了线程安全,可是这种写法依旧存在一个问题,那就是加锁的代价过高,在下面代码中函数

singleton* singleton::get_singleton(int data = 0)
{
    lock_guard<mutex> lock(mut);//上锁与自动解锁
    if (point == nullptr)
    {
        point = new singleton(data);
    }
    return point;
};

全部的线程都须要进行上锁与解锁操做,即便是point != nullptr。这个很不合理,由于仅仅在point == nullptr时候才须要加锁,考虑到这个缘由须要在point != nullptr不进行加锁。
三、双检查锁的线程安全(可是因为内存读写reorder,不能用,不安全);
为了提高效率,在point != nullptr不进行加锁,那么写成以下形式正确吗?
很明显是存在问题的,这样写的话若全部线程都在point == nullptr挂起直接就让锁失效了。优化

singleton* singleton::get_singleton(int data = 0)
{
    if (point == nullptr)
    {
        lock_guard<mutex> lock(mut);//上锁与自动解锁
        point = new singleton(data);
    }
    return point;
};

正确的写法应该为:ui

singleton* singleton::get_singleton(int data = 0)
{
    if (point == nullptr)
    {
        lock_guard<mutex> lock(mut);//上锁与自动解锁
        if(point==nullptr)
            point = new singleton(data);
    }
    return point;
};

在加锁后进行二次判断point == nullptr,这样就可以达到逻辑上的正确了。
可是依旧存在问题:因为new的操做与底层编译器有关,当底层的编译器按照malloc->构造函数->赋值给point的顺序来执行,可是某些编译器为了优化会采用malloc->赋值给point->构造函数的顺序来执行,这就是reorder的问题,当线程1在完成赋值给point过程却被挂起,线程2开始执行这个代码的时候检查第一个point == nullptr?其会发现point != nullptr,所以其会返回一个未经构造函数构造的对象内存。
四、C++11的跨平台双检查锁的线程安全,使用的是atomic(屏蔽编译器的reorder);
修改后的代码以下:this

#ifndef SINGLETON_H
#define SINGLETON_H
#include<iostream>
#include<thread>
#include<mutex>
#include<atomic>
using std::mutex;
using std::lock_guard;
class singleton
{
private:
    singleton(int data=0):data(data)
    { std::cout << "建立构造函数" << std::endl; };
    singleton(const singleton&) {};
    ~singleton() {};
    static singleton* point;
    int data;
public:
    static singleton* get_singleton(int);
    void set_singleton(int);
    void get_data();
};
static mutex mut;
static std::atomic<singleton*> s_p;
singleton* singleton::point= nullptr;
singleton* singleton::get_singleton(int data = 0)
{
    singleton* point = s_p.load(std::memory_order_relaxed);
    std::_Atomic_thread_fence(std::memory_order_acquire);
    if (point== nullptr)
    {
        lock_guard<mutex> lock(mut);//上锁与自动解锁
        if(point ==nullptr)
            point = new singleton(data);
        std::_Atomic_thread_fence(std::memory_order_release);
        s_p.store(point, std::memory_order_relaxed);
    }
    return point;
};
void singleton::set_singleton(int data) 
{
    this->data = data;
}
void singleton::get_data() 
{
    std::cout << "the data is : " << this->data<<std::endl;
}
#endif // !SINGLETON_H

固然在JAVA中存在volatile,而c++中没有。
五、懒汉模式与饿汉模式
上述的都是懒汉模式,即要到使用了才建立对象,还有一种饿汉模式,其是在使用该单一对象前就建立了这个对象,以下所示:atom

class singleton
{
protected:
    singleton()
    {};
private:
    static singleton* p;
public:
    static singleton* initance();
};
singleton* singleton::p = new singleton();
singleton* singleton::initance()
{
    return p;
}

这个是绝对的线程安全的,实际上对于开发者而言饿汉模式恰巧是最懒的,可是存在的问题是,若是这个单一类须要入口参数,那该怎么办?线程

3、要点总结

一、Singleton模式中的实例构造器能够设置为protected以容许子类派生;二、Singleton模式通常不要支持拷贝构造函数和Colne接口,由于这有可能致使多个对象实例,与Singleton模式的初衷违背;三、如何实现多线程下的Siglenton?注意对双检查锁的正确实现;四、懒汉模式与饿汉模式的区别。

相关文章
相关标签/搜索