这篇文章不讲什么是单例模式,什么是饿汉和懒汉,假定读者已经知道这些。
我们知道,最朴素的懒汉单例模式是线程不安全的,这篇文章逐步将其升级,直到实现一个完全线程安全的。
另外,线程安全的懒汉式单例的实现依赖于特定语言,不同的语言有不同的实现,比如 Java 中依赖其虚拟机的实现,而本文要说的 C++ 实现则依赖标准库。
1. 朴素的单线程懒汉式单例实现
我们先看一个最朴素的实现:
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 29
| #include <iostream>
class LazySingleton { static LazySingleton* instance; LazySingleton() {} LazySingleton(LazySingleton const &) = delete; LazySingleton& operator=(LazySingleton const &) = delete;
public: static LazySingleton* get_instance() { if (instance == nullptr) { instance = new LazySingleton(); } return instance; } };
LazySingleton* LazySingleton::instance;
int main(void) {
LazySingleton* a = LazySingleton::get_instance(); LazySingleton* b = LazySingleton::get_instance();
std::cout << a << std::endl; std::cout << b << std::endl;
return 0; }
|
输出:
可以看出指针 a
和 b
的值相同,单例模式没有问题。
下面,我们将重点关注 LazySingleton 类在多线程环境下的对象创建操作,讨论此操作的线程安全性,且假定其他操作均是线程安全的。
2. 支持多线程的懒汉式单例
显然,上面的单例设计只适用于单线程程序,其是线程不安全的,有可能会实际创建多个对象。
想要是上面的程序线程安全,最简单直观的方法就是加锁,互斥锁。
我们看代码:
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 29 30 31 32 33
| #include <iostream> #include <mutex>
class LazySingleton { static LazySingleton* instance; static std::mutex m; LazySingleton() {} LazySingleton(LazySingleton const &) = delete; LazySingleton& operator=(LazySingleton const &) = delete;
public: static LazySingleton* get_instance() { std::lock_guard lk(m); if (instance == nullptr) { instance = new LazySingleton(); } return instance; } };
LazySingleton* LazySingleton::instance; std::mutex LazySingleton::m;
int main(void) {
LazySingleton* a = LazySingleton::get_instance(); LazySingleton* b = LazySingleton::get_instance();
std::cout << a << std::endl; std::cout << b << std::endl;
return 0; }
|
这段代码使用了 std::mutex
互斥,使得单例类线程安全了,现在可以保证是真正的“单例”了。
但问题也很明显,一旦有一个线程获取了锁,直到其释放锁前,其他线程都会阻塞。也就是说,即便单例对象早已经创建完成,其他的线程进行到这的时候还是经常会被阻塞,并发性能极差。如果并发线程多,可能导致大量线程在这里阻塞,甚至拖慢整个系统的效率。
3. 双重检验锁定模式(double-checked locking pattern)改进
双重检验锁定模式简单地说,与上面的方法的区别是:
上面的方法中,是先获取锁,再检查空指针,如果指针为空,则创建对象实例;
而双重检验锁定模式重,先检查一次空指针,如果指针为空,则获取锁,再检查指针,如果仍为空,则创建对象实例。
我们修改 get_instance()
方法如下:
1 2 3 4 5 6 7 8 9
| static LazySingleton* get_instance() { if (instance == nullptr) { std::lock_guard lk(m); if (instance == nullptr) { instance = new LazySingleton(); } } return instance; }
|
这个改动的好处是,只有当指针为空的时候,才会获取锁。而在之前的方法中,不管指针是否为空都要先获取锁。
而在获取锁后,还会再判断一次(即双重检验)指针是否为空,这是防止在第一个判断和获取锁之间,有其他进程改动过指针。
此方法减少了获取锁的可能,就减少了线程并发的代价,同样实现了线程安全,且有一定的性能提升。不过我认为这个优化带来的性能提升还是比较有限的。
-
但是,请注意,双重检验锁定模式并非适合所有场景。
双重检验锁定模式多用于延迟初始化,我们上面的懒汉式单例也属于这种,只是过于简单,其创建实例的过程非常快,难以体现出此方法的弊端。
假设我们需求如下:通过 get_instance()
方法获取单例实例,如果实例不存在(指针为空),则创建实例,对实例对象进行一些初始配置(修改),然后返回指向实例的指针。
需求看似很简单,但如果我们再加一条要求呢:初始化配置过程应该只进行一次,且耗时非常长! 这并不是什么苛刻离谱的要求,而是一个合理合情的需求。
想象一下,如果我们将耗时很长的初始化过程也放在获取锁内的时间内执行,固然可以完成上述需求,但此时除了正在执行初始化配置以外的其他线程,调用 get_instance()
时都会阻塞,最终结果就是大量线程阻塞,导致系统整体性能下降。所以我们需要将耗时的初始化过程放在锁外进行。
但锁外执行初始化操作,也存在问题,我们看下面的代码:
1 2 3 4 5 6 7 8 9 10 11
| static LazySingleton* get_instance() { if (instance == nullptr) { std::lock_guard lk(m); if (instance == nullptr) { instance = new LazySingleton(); } } return instance; }
|
假设我们现在不存在实例,有两个线程 A 和 B 开始调用 get_instance()
。
- 线程 A 执行到第 2 行的
if (instance == nullptr)
,然后线程 B 开始执行;
- 线程 B 一口气执行完了
get_instance()
全部的内容,即最终创建了一个单例对象,并做了一些初始配置(由第 7、8 行注释表示),并给调用者返回了指向实例的指针。
- 线程 A 继续执行,执行到第二重检查时(第 4 行),发现指针已非空,于是跳到了第 7 行,开始初始化配置。
问题就在线程 A 最后这个初始化配置,线程 B 将实例指针返回给调用者后,该实例是有可能被修改的。而线程 A 在我们计划外的,对实例进行了第 2 次初始化,使得结果偏离预期。
上述场景,双重检验锁定模式就不合适了,我们看下一个。
4. 利用 std::call_once() 函数实施线程安全的延迟初始化
在讲这一节前,我们需要先介绍一下,std::once_flag
类 和 std::call_once()
函数,他们均在 C++11 中引入,在 “mutex” 头文件中。
std::once_flag
类是函数 std::call_once()
的辅助类,传递给多个 std::call_once()
调用的 std::once_flag
对象让这些调用相互协调,使得最终仅有一个调用真正运行完成。这个类不可复制亦不可移动。
std::call_once()
方法接受一个 std::once_flag
引用,以及一个可调用类型 f
及其参数。如果有多个 std::call_once()
调用接受了同一个 std::once_flag
引用,那么所有的 std::call_once()
调用只会有一个执行完成。另外要注意,如果多个传入的函数调用不一样,也是只会调用一次,但实际调用哪个是不确定的,属于未定义行为。
函数 std::call_once()
的声明如下:
1 2
| template< class Callable, class... Args > void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
|
Callable 指的是任何可调用类型(callable type,包含函数指针、函数对象、lambda 等,能让适用者对其进行函数调用操作)。
总之,到这里,我们知道了一件事:C++11 标准库中提供了方法,可以让某件事只做一次(比如初始化一次),不管调用了几次,不管是不是多线程调用的,最终一定只执行一次。并且可以保证这一次调用执行完成后,所有线程才会继续推进。
4.1. 针对第 2 小节的问题优化
在 2. 小节中,我们说过那个方法会可能导致有很多线程阻塞。这个问题的主要原因是,我们先获取锁,再检查指针,导致不必要的获取锁和阻塞。
我们这里通过一个 create_instance_flag
,让多个线程都调用 std::call_once()
,保证对象的创建由其中某线程安全唯一的完成(通过合适的同步机制,有可能是系统提供的方法,也可能是加锁等)。这样做的性能比其他同类操作要好。
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
| #include <iostream> #include <mutex> #include <thread>
class LazySingleton { static LazySingleton* instance; static std::once_flag create_instance_flag; LazySingleton() {} LazySingleton(LazySingleton const &) = delete; LazySingleton& operator=(LazySingleton const &) = delete; static void create_instance() { instance = new LazySingleton(); }
public: static LazySingleton* get_instance() { std::call_once(create_instance_flag, create_instance); return instance; } };
LazySingleton* LazySingleton::instance; std::once_flag LazySingleton::create_instance_flag;
int main(void) {
return 0; }
|
4.2. 针对第 3 小节的问题优化
对于第 3 小节后面提到的,将耗时的初始化内容与对象的创建操作分离,但同时保证只执行一次,我们可以再加入一个 init_instance_flag
用于保证这一点。
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 29 30 31 32 33 34 35
| #include <iostream> #include <mutex> #include <thread>
class LazySingleton { static LazySingleton* instance; static std::once_flag create_instance_flag; static std::once_flag init_instance_flag; LazySingleton() {} LazySingleton(LazySingleton const &) = delete; LazySingleton& operator=(LazySingleton const &) = delete; static void create_instance() { instance = new LazySingleton(); } static void init_instance() { }
public: static LazySingleton* get_instance() { std::call_once(create_instance_flag, create_instance); std::call_once(init_instance_flag, init_instance); return instance; } };
LazySingleton* LazySingleton::instance; std::once_flag LazySingleton::create_instance_flag; std::once_flag LazySingleton::init_instance_flag;
int main(void) {
return 0; }
|