单例模式的优点是只有一个对象,可以节省资源,提高运行速度,提供一个全局的访问点。
在使用过程中,写一个具有单例模式思想的程序可以很简单,但是如果要做到线程安全并且生成实例的过程中符合业务需求,是一件困难的事情。
懒汉模式,会将实例化的时间延迟到正式调用接口的时候。
简单的实现方式
class singleton
{
public:
singleton* getInstance()
{
if(p_singletonIns == nullptr)
{
p_singletonIns = new singleton();
}
return p_singletonIns;
}
private:
singleton()=default;
singleton(const singleton &cpSingleton) = delete;
singleton& operator=(const singleton& cpSingleton) = delete;
static singleton* p_singletonIns;
};
singleton* singleton::p_singletonIns = nullptr;
上面的用例中,可以看到上面的p_singletonIns的在第一次调用getInstance()的时候会将p_singletonIns赋予一个实例的地址。这种延迟到使用时才进行实例化的做法,叫做懒汉模式。
但是这种方法不是线程安全的,同时会存在内存泄露的问题。
使用指针的方式的话,在创建实例的时候一般使用DCL(Double Check Lock), 双检测锁。代码如下所示
class singleton
{
public:
singleton* getInstance()
{
if(p_singletonIns == nullptr) // p1
{
std::lock_guard<std::mutex> lock();
if(p_singletonIns == nullptr) // p2
{
p_singletonIns = new singleton(); // p3
}
}
return p_singletonIns;
}
private:
singleton()=default;
singleton(const singleton &cpSingleton) = delete;
singleton& operator=(const singleton& cpSingleton) = delete;
static singleton* p_singletonIns;
};
singleton* singleton::p_singletonIns = nullptr;
但是在使用过程中, p1的判断是存在一个风险,线程仍然不安全。就是在p3在new的时候,在new这个过程中,p_singletonIns的指针会被先赋值,然后开始创建内存。在并发的情况下,p1可能判断出p_singletonIns不是空指针,但是内存其实没有完全分配完成,那么造成返回p_singletonIns这个指针其实是不可用的。
在C++11的标准库中有原子变量的实现。使用原子变量就可以上述这种避免线程不安全的问题。
std::mutex mutexIns;
class singleton
{
public:
static singleton* getInstance()
{
if(p_singletonIns.load() == nullptr) // p1
{
std::lock_guard<std::mutex> lock(mutexIns);
if(p_singletonIns.load() == nullptr) // p2
{
p_singletonIns.store(new singleton()); // p3
}
}
return p_singletonIns.load();
}
private:
singleton()=default;
singleton(const singleton &cpSingleton) = delete;
singleton& operator=(const singleton& cpSingleton) = delete;
static std::atomic<singleton*> p_singletonIns;
};
std::atomic<singleton*> singleton::p_singletonIns(nullptr);
使用局部静态变量也可以实现懒汉模式,使用局部静态变量在C++11之后是线程安全。这也是最为优雅的一种方式。
class singleton
{
public:
static singleton* getInstance()
{
singleton singletonIns;
return &singletonIns;
}
private:
singleton()=default;
singleton(const singleton &cpSingleton) = delete;
const singleton& operator=(const singleton& cpSingleton) = delete;
};
饿汉模式:故名思意,一个饿坏的人会将一切能吃的东西都塞到肚子里,吃不下去的也要拽在手里。
在编程中这种模式的体现就在于资源在程序启动的时候就会分配好。而不是在运行时才进行资源分配。
使用饿汉模式编写单例模式相对简单一些。
class singleton
{
public:
singleton* getInstance()
{
return &singletonIns;
}
private:
singleton()=default;
singleton(const singleton &cpSingleton) = delete;
singleton& operator=(const singleton& cpSingleton) = delete;
static singleton singletonIns;
};
singleton singleton::singletonIns;
使用饿汉模式的话,singletonIns在C++中进行初始化的过程发生在程序加载的过程中,在main运行之前,也在调用getInstance()之前。这个初始化的过程也可以知道是确保线程安全的。同时也可以保证在程序结束之后自动析构。
首先资源泄露并不只是意味着内存泄露,还会指网络资源的释放,比如在分布式系统中,在程序退出时,让其他系统知道程序的退出等等场景。
在单例模式中,因为使用全局变量或者静态变量进行构造的时候,在程序退出时,会自动进行析构,通常是不存在资源泄漏的。在使用new的情况下,则不会自动进行析构,会存在资源泄漏的问题。下面介绍使用堆内存来构造单例模式的时候,如何在程序推出时,自动析构。
使用嵌套的对象变量来释放,是因为单例模式的析构函数是private权限的,因此要使用嵌套对象来解决这个private的访问权限的问题。也满足高内聚编程原则。
std::mutex mutexIns;
class singleton
{
public:
class deletor
{
public:
~deletor()
{
singleton *singletonIns;
if((singletonIns = getInstance()) != nullptr)
{
delete singletonIns;
}
}
};
static singleton* getInstance()
{
if(p_singletonIns.load() == nullptr) // p1
{
std::lock_guard<std::mutex> lock(mutexIns);
if(p_singletonIns.load() == nullptr) // p2
{
p_singletonIns.store(new singleton()); // p3
}
}
return p_singletonIns.load();
}
private:
singleton()=default;
singleton(const singleton &cpSingleton) = delete;
singleton& operator=(const singleton& cpSingleton) = delete;
~singleton()
{
//do anything to make sure you have prevented resource leak.
//...
}
static std::atomic<singleton*> p_singletonIns;
static deletor deletorIns;
};
std::atomic<singleton*> singleton::p_singletonIns(nullptr);
singleton::deletor deletorIns;