前言
单例模式(Singleton)几乎是人尽皆知的设计模式了,它限制一个对象只能实例化一次,且该对象的生命周期一般与整个应用的生命周期一致(否则,单例模式完全可以被普通类对象替代)。单例对象应该允许多线程访问,确保单例对象是线程安全的十分有必要。
单例由于其生命周期特点,一般被实现为指针对象或静态对象,下面将分别讨论这两种情况的线程安全实现。
返回静态指针
下面实现一个基本的单例对象:
class Singleton
{
private:
Singleton() = default;
static Singleton* singleton_;
public:
Singleton(Singleton& other) = delete;
void operator=(const Singleton&) = delete;
static Singleton* Instance() {
if (singleton_ == nullptr) {
singleton_ = new Singleton();
}
return singleton_;
}
};
Singleton* Singleton::singleton_= nullptr;;
上面的例子中,Singleton 类的构造函数被隐藏,不允许用户直接实例化,同时禁止对象的拷贝。注意该 Singleton 有个类型为 Singleton 的静态成员指针变量,指向在静态成员函数 Instance() 中被实例化的对象,该对象只能被实例化一次,并在创建对象后将指针返回。这就实现了一个看起来还不错的单例对象。
但是,我们只看到了 new 操作符,而没有 delete ,上述代码将造成内存泄露。让我们增加下面的代码。
class Singleton
{
private:
~Singleton(){
if (singleton_) {
delete singleton_;
}
}
}
是不是这样就可以了呢?不,这不仅没有解决问题,反而引入了更严重的漏洞。 首先 singleton_ 指针是 static 对象,对象被析构和内存被释放的时机是不确定的;其次我们在析构函数中 delete singleton_ ,而该操作会再次调用析构函数,淦,递归死循环,这真是灾难。
正确的做法是,将 delete 操作放到静态成员函数中,由用户在合适的时机释放资源(用户有责任这么做),例如在退出 main() 函数前调用 Singleton::DestoryInstance() ,实现如下(Example 3 ):
class Singleton
{
public:
static DestoryInstance() {
if (singleton_) {
delete singleton_;
singleton = nullptr;
}
}
}
目前为止,这个单例类我们实现的还算完整,它在单线程应用程序可以安全工作,但多线程环境中仍有风险。
考虑到 singleton_ 在申请资源(new 操作)完成之前,多个线程可能已经完成了 singleton_ == nullptr 的判断条件,此时这些线程都将执行 new 操作,这就创建了多个 Singleton 实例,导致单例模式设计失败。
解决方案就是引入同步化技术,也是将程序块 if (singleton_ == nullptr) { singleton_= new Singleton() } 变成同步(原子)操作,一次值允许一个线程执行。实现如下(Example 4 ): 值得一提的事,在加锁之前就进行了一次条件判断(singleton_ == nullptr ),这是为了在多线程中,避免不必要的加锁等待,提高程序运行效率。
class Singleton
{
private:
Singleton() = default;
static Singleton* singleton_;
static std::mutex mtx;
public:
Singleton(Singleton& other) = delete;
void operator=(const Singleton&) = delete;
static Singleton* Instance() {
if (singleton_ == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (singleton_ == nullptr) {
singleton_ = new Singleton();
}
}
return singleton_;
}
static DestoryInstance() {
if (singleton_) {
delete singleton_;
singleton = nullptr;
}
}
};
Singleton* Singleton::singleton_= nullptr;;
返回静态引用
将单例对象声明为静态存储区变量的实现如下:
class Singleton
{
private:
Singleton() = default;
public:
Singleton(Singleton& other) = delete;
void operator=(const Singleton&) = delete;
static Singleton& Instance() {
static Singleton singleton_
return singleton_;
}
};
以上代码看起来过于简单,而且非线程安全。实则不然,在 C++11 之后,静态存储期变量的初始化细节有所变化,即使在多线程环境中,多线程同时初始化,也能保证静态变量只会初始化一次。编译器已经为我们做了上述中双重检查锁的工作。
比较以上两种实现,Instance() 返回指针的方式的唯一优点是用户可以在程序退出之前多次销毁和重建单例对象,当然这和单例模式的概念冲突。在 Modern C++ 编程中,返回局部静态对象的引用,是更加简洁高效的手段。
单例模板
当一个程序有多个单例对象需要实现时,构造一个单例模板类,让其他单例类继承该模板基类,能节省不少时间。
template <class T>
class SingletonBase
{
protected:
SingletonBase() {}
public:
SingletonBase(SingletonBase const &) = delete;
SingletonBase& operator=(SingletonBase const&) = delete;
static T& instance()
{
static T single;
return single;
}
};
一个简单的实例:
class ConsoleLogger : public SingletonBase<ConsoleLogger>
{
private:
Single() {}
friend class SingletonBase<ConsoleLogger>;
public:
void Debug() { std::cout << "debug" << std::endl; }
};
由于基类需要访问派生类的私有默认构造函数,所以基类是派生类的一个友元类。
C/C++Linux服务器开发/后台架构师【零声教育】 : https://ke.qq.com/course/417774?flowToken=1041371
|