软件设计模式之单例模式

什么是单例模式?

有的时候我们需要应用程序中的某个实例在运行期间有且只能有一个实例,程序运行时没有任何方法实现创建多于一个的实例,这种情况我们称之为软件设计模式——单例模式。

比如我们软件运行时,有且只能有一个日志对象…….

单例模式实现

单例模式实现时将类的构造函数私有化,使得外部方法无法通过类的构造函数构造对于一个的类对象,从而保证对象的唯一性。

单例模式分类

软件设计模式的单例模式实现有两种:

  • 饿汉模式:类定义的时候实现对象实例化(饿汉,很饿,要马上吃饭)。
  • 懒汉模式:类使用的时候实现对象实例化(懒汉,不那么饿,稍后再吃)。

单例模式之饿汉实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//单例模式 饿汉实现
class SingletonHungry
{
private:
//构造函数私有化,不允许外部方法通过new来创建实例
SingletonHungry(){}
static SingletonHungry *p1; //静态成员变量声明

public:
static SingletonHungry *getInstance();
};
SingletonHungry *SingletonHungry::p1 = new SingletonHungry(); //静态成员变量的定义及初始化
SingletonHungry *SingletonHungry::getInstance()
{
printf("单例模式,饿汉实现进行对象实例化\n");
return p1;
}

单例模式的饿汉实现中在类定义时已经实现的对象的实例化,所以对于单线程/多线程的环境都是安全的。

单例模式之懒汉实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//单例模式 懒汉模式
class SingletonLazy1
{
private:
//构造函数私有化
SingletonLazy1(){}
static SingletonLazy1 *p2; //静态成员变量声明

public:
static SingletonLazy1 *getInstance();
};
SingletonLazy1 *SingletonLazy1::p2 = nullptr; //静态成员变量的定义及初始化
SingletonLazy1 *SingletonLazy1::getInstance()
{
printf("单例模式,懒汉实现进行对象初始化\n");
{
if(p2 == nullptr)
{
return new SingletonLazy1();
}
return p2;
}
}

很明显,对于上面的懒汉模式代码实现单线程运行时毫无疑问是正确的,但是多线程环境下,会创建多于一个的对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//单例模式 懒汉模式,加锁机制实现线程安全
class SingletonLazy1
{
private:
//构造函数私有化
SingletonLazy1(){}
static SingletonLazy1 *p2; //静态成员变量声明
static mutex lock_; //静态成员变量声明

public:
static SingletonLazy1 *getInstance();
};
SingletonLazy1 *SingletonLazy1::p2 = nullptr; //静态成员变量的定义及初始化
std::mutex SingletonLazy1::lock_; //静态成员变量的定义
SingletonLazy1 *SingletonLazy1::getInstance()
{
printf("单例模式,懒汉实现进行对象初始化\n");
{
//对象实例化加锁,保证线程安全
lock_guard<mutex> my_lock(lock_);
if(p2 == nullptr)
{
return new SingletonLazy1();
}
return p2;
}
}

以上单例模式懒汉实现代码中,对于对象实例的部分加锁,这样即使在多线程运行时,也能够保证程序运行时只创建一个对象。

但是,可以观察到这种方法虽然安全却不高效,因为每次运行到实例化对象的这部分代码时都需要加锁,但其实我们只需要在第一次运行此部分代码时需要加锁,此后运行的任何一次都不需要加锁,所以这种实现方法导致性能浪费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//单例模式 懒汉模式,DLCP(双重检查锁模式----Double-Checked Locking Pattern)实现线程安全
class SingletonLazy2
{
private:
//构造函数私有化
SingletonLazy2(){}
static SingletonLazy2 *p3; //静态成员变量声明
static mutex lock_; //静态成员变量声明

public:
static SingletonLazy2 *getInstance();
};
SingletonLazy2 *SingletonLazy2::p3 = nullptr; //静态成员变量的定义及初始化
std::mutex SingletonLazy2::lock_; //静态成员变量的定义
SingletonLazy2 *SingletonLazy2::getInstance()
{
printf("单例模式,懒汉实现进行对象实例化DCLP\n");
if(p3 == nullptr)
{
lock_.lock();
if(p3 == nullptr)
{
p3 = new SingletonLazy2();
}
lock_.unlock();
}
return p3;
}

所谓单例模式懒汉DCLP实现是指实例化对象时进行两次判空操作(上述代码第18,21行),为什么需要两次判空操作?🤦‍♂️🤦‍♂️🤦‍♂️

如果没有第18行的判空操作,那么上述代码与普通加锁实现无异,多线程安全但性能不高。

如果没有第21行的判空操作,假设有两个线程thread1、thread2都执行了第18行,但是thread1获得CPU时间片开始执行第20,23,25行,成功创建了一个对象,然后thread2获得CPU时间片,再次执行第20,23,25行,导致程序中出现多个对象实例。

所以,两次判空不可少!!!

上述DCLP实现看起来完美,但是,代码运行时并没有想象的那么完美,代码中存在重大漏洞,原因是:内存读写的乱序执行(编译器问题)

那到底怎么解决???😢😢😢算了写不下去了,贴个连接备忘一下,我太菜了,其实是没看懂解决方法。

C++11标准中定义了线程、原子操作以及新的内存模型,通过这些新增的标准可以正确的实现单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::atomic<Singleton*> Singleton::instance_;
std::mutex Singleton::m_mutex;

Singleton* Singleton::Instance() {
Singleton* temp = instance_.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (temp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
temp = instance_.load(std::memory_order_relaxed);
if (temp == nullptr) {
temp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);
instance_.store(temp, std::memory_order_relaxed);
}
}
return temp;
}

参考文章

C++设计模式之单例模式

大话设计模式之单例模式

C++ 多线程互斥锁(mutex,lock,lock_guard)

C++11:原子操作

C++11 修复了双重检查锁定问题

C++中的Singleton(单例模式)及其实现%E5%8F%8A%E5%85%B6%E5%AE%9E%E7%8E%B0.md)

Author: wnxy
Link: https://wnxy.xyz/2021/11/10/cpp_singleton_pattern/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.