1 Singleton介绍
Singleton即为单例模式,是众多设计模式的其中一种。这种模式主要是为了保证一个类在一个系统中仅有一个实例,并提供一个访问它的全局访问点。
那么为什么要设计单例模式呢?试想一下,假如一个系统中有一个日志类 class logger
,没有采用单例模式的设计;这个日志类可以把系统的行为记录和异常信息等写入到txt文件中。这个系统又有两个进程Thread 1和Thread 2,此时这两个线程分别实例化了logger
对象,并调用这个日志类logger
的成员函数向日志文件中写入信息,两个进程同时向一个文件进行写操作,肯定会出现问题,执行速度快的进程写的信息会被执行速度慢的进程覆盖。怎么解决这个问题呢?当然,加入互斥锁是一个方法,但是会比较麻烦。一个比较好的解决方法是将logger
类设计成单例类,一个系统中只能存在一个。
单例模式相对于加锁的好处是,不用创建那么多 logger
对象,一方面节省内存空间,另一方面节省系统文件句柄。(对于操作系统来说,文件句柄也是一种资源,不能随便浪费)
2 如何实现Singleton
只需要3步即可:
- 将构造函数,拷贝函数,赋值函数私有化,从而禁止外界创建实例。
- 在类中指定一个静态的指向本类型的指针变量。
- 提供静态成员函数作为实例全局访问点,该方法返回对该类唯一的实例的引用。
需要注意的是getInstance()
函数返回的是对实例的引用,如果返回的是指针的话有被外部代码delete
的风险。
3 几种单例模式
3.1 懒汉模式(Lazy Singleton)
//头文件中
class Singleton
{
public:
static Singleton& getInstance()//注意 返回的是引用
{
if (instance_ == NULL)
{
instance_ = new Singleton;
}
return *instance_;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
private:
static Singleton* instance_;
};
//实现文件中
Singleton* Singleton::instance_ = 0;
注意到此种模式直到getInstance()
被访问,才会生成实例,这种特性被称为延迟初始化(Lazy initialization),这在一些初始化时消耗较大的情况有很大优势。
Lazy Singleton不是线程安全的,比如现在有线程A和线程B,都通过了instance_ == NULL
的判断,那么线程A和B都会创建新实例。单例模式保证生成唯一实例的规则被打破了。
3.2 饥汉模式(Eager Singleton)
//头文件中
class Singleton
{
public:
static Singleton& getInstance()
{
return instance;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
private:
static Singleton instance;
}
//实现文件中
Singleton Singleton::instance;
由于在main函数之前初始化,所以没有线程安全的问题,但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元(可理解为cpp文件和其包含的头文件)中的初始化顺序是未定义的。如果在初始化完成之前调用getInstance()
方法会返回一个未定义的实例。
3.3 Meyers Singleton
Scott Meyers在《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用local static对象(函数内的static对象)。当第一次访问getInstance()
方法时才创建实例。
class Singleton
{
public:
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
};
以下是两种线程安全的实现。
3.4 双检测锁模式(Double-Checked Locking Pattern)
双检测锁模式(Double-Checked Locking Pattern)是针对懒汉模式线程安全的改造。
上面说过假如有线程A和线程B,都通过了instance_ == NULL
的判断,那么线程A和B都会创建新实例。为了解决这个问题,自然而然想到对该判断加锁,多个进程不能同时进行该判断,那么便可以实现线程安全,这个思路的实现如下:
static Singleton& getInstance()
{
if (instance_ == NULL)
{
Lock lock; //基于作用域的加锁,超出作用域,自动调用析构函数解锁
if (instance_ == NULL)
{
instance_ = new Singleton;
}
}
return *instance_;
}
实际上只有该对象刚创建的时候需要互斥访问判断instance_ == NULL
,其余时间可以不用加锁解锁操作。而在实际执行过程中,即便实例对象创建成功之后,各个进程也会频繁的加锁解锁,很影响系统的运行效率。为了改善这种情况,可以在加锁之前再进行一个判断,假如对象已经被创建便可以直接返回,如果没有被创建,再进行互斥访问加锁操作。改进之后便是双检测锁模式,代码如下:
static Singleton& getInstance()
{
if (instance_ != NULL) {
return *instance_;
} else
{
Lock lock; //基于作用域的加锁,超出作用域,自动调用析构函数解锁
if (instance_ == NULL)
{
instance_ = new Singleton;
}
}
return *instance_;
}
双检测模式逻辑上没有问题,但是在实际的代码执行的过程中还是存在很多问题,如指令重排、多核处理器等问题让DCLP实现起来比较复杂,比如需要使用内存屏障,详细的分析可以阅读这篇论文。
3.5 pthread_once模式
pthread_once 一般用于一次性的线程初始化,在整个声明周期中,该方法只执行一次,从而实现一种线程安全的单例模式。
C++的pthread_once () 函数 :
int pthread_once(pthread_once_t *once_control, void(*int_routine)(void));
once_control : 一个静态或全局变量,初始化为 PTHREAD_ONCE_INT
init_routine : 初始化函数的函数指针
返回值:成功: 0 错误:错误代码
muduo 网络库的 Singleton 用到了 pthread_once。
template<typename T>
class Singleton : boost::nocopyable{
public:
static T& instance(){
pthread_once(&ponce_,&Singleton::init);
return *value_;
}
private:
Singleton();
~Singleton();
static void init(){
value_ = new T();
}
private:
static pthread_once_t ponce_;
static T* value_;
};
// 必须在头文件中定义static 变量
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;
template<typename T>
T* Singleton<T>::value_ = NULL;
这里的boost::noncopyable的作用是把构造函数, 赋值函数, 析构函数, 复制构造函数声明为私有或者保护。
4 总结
单例模式的单线程实现比较简单,多线程实现比较麻烦。双检测锁模式在实际的运行过程中可能会受底层硬件指令重排的影响而产生非预期结果。Java为了解决指令重排的问题,在JDK1.5之后,可以使用volatile变量禁止指令重排序,让DCL生效。c++则是通过pthread_once模式解决了线程安全问题。