0%

设计模式之单例模式

我们从 GoF 说起,书中写到单例模式(Singleton)的意图是:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

在什么时候会用到单例模式呢?开发过程中常见的应用举例:

  • 日志类,一个应用往往只对应一个日志实例:丑陋的写法是在每个场景都实例化一个日志对象使用。
  • 配置类,应用的配置集中管理,并提供全局访问:丑陋的写法是在每次使用配置时都实例化一个配置对象,然后访问配置文件读取其中的配置项。
  • 管理器,比如我们写了一个 windows 服务,然后我们要封装一个启停此服务的管理器。
  • 共享资源类,加载资源需要较长时间,使用单例可以避免重复加载资源,并被多个地方共享访问。比如文件连接,数据库连接等

下面来看具体的实现。在 C++ 中,虽然将一个对象定义成全局变量可以使其被全局访问,但它不能防止你实例化多个对象,即无法“保证一个类仅有一个实例”。如果你坚持如此使用也能实现功能,但并不优雅。

更好的办法是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建(通过截取创建新对象的请求),并且它可以提供一个访问该实例的办法。

以上描述中涉及 3 个功能点,我们来看看在 C++ 中具体是如何实现的?

1
2
3
4
5
6
7
class Singleton {
private:
static Singleton* _instance; //类自身保存它的唯一实例
Singleton(); // 截取创建新对象的请求
public
static Singleton* Instance(); // 提供一个访问该实例的办法
};

上述定义中未体现类的唯一实例怎么创建的。而恰恰是如何创建实例区分出了以下几种实现:

  1. Lazy Singleton,命名来源于其中使用了惰性(Lazy)初始化
  2. 双检测锁模式,在上述基础上为线程安全作出修改
  3. Eager Singleton,不使用惰性初始化
  4. Meyers Singleton,在新标准下支持多线程,最优雅的一种实现。

需要强调的是,虽然上述几种实现都是由 Singleton 类创建实例,但这并不是必须的。GoF 书中描述 Singleton 类的职责时也只是“可能负责创建它自己的唯一实例”,事实上,在书中讲述单件注册表时就明确提到了:(详细内容请翻看原著)

Singleton 类不再负责创建单件。

为了保证唯一实例,还需要删除拷贝构造函数(和拷贝赋值操作符)。另外,用户获得指针之后可能会以为他负责释放,进而对 Singleton::Instance() 的结果执行 delete 操作,如果我们并不希望如此,可以将析构函数声明为私有的,或者不使用指针(改用引用)。

1
2
3
4
5
6
7
8
9
10
11
12
Singleton* pSingleton = Singleton::Instance();
// 以下两个操作是违背单例模式特性的,必须禁止掉。
Singleton singleton = *pSingleton;
Singleton singleton2(*pSingleton);

// 但是这个样子可以
Singleton& refSingleton = *pSingleton;
Singleton& refSingleton2(*pSingleton);

cout << "singleton'addr is: " << &singleton << endl;
cout << "pSingleton is: " << pSingleton << endl;
cout << "singleton2'addr is: " << &singleton2 << endl;

建议将移动构造函数、移动赋值操作符也禁用,减少错误使用的可能性。否则,在某个模块中将单例移走之后,另一模块大概率会错误使用。

单线程 Lazy Singleton

在 GoF 书中给出的实现如下:

1
2
3
4
5
6
7
8
9
10
Singleton* Singleton::_instance = 0;

Singleton::Singleton() {}

Singleton* Singleton::Instance() {
if (_instance == 0) {
_instance = new Singleton;
}
return _instance;
}

这种实现被称为“Lazy Singleton”,它使用了惰性初始化:它的返回值直到被第一次访问时才创建和保存。

惰性初始化:实例直到用到的时候(用户通过 Singleton::Instance() 要获取实例时)才创建,在此之前实例对象是不存在的,避免了潜在浪费资源的可能。对比非惰性初始化的 Eager Singleton 理解,提前把对象初始化,但应用的执行逻辑可能就没有用到此实例。

但正如 GoF 在第一章引言中指出的一样,上述实现也存在局限。

书中没有讨论与并发或分布式或实时程序设计有关的模式,也没有收录面向特定应用领域的模式。

最突出的就是非线程安全。另外在部分使用场景中,我们还需要考虑 Singleton 的释放问题。

线程安全

Lazy Singleton不是线程安全的,比如现在有线程 A 和线程 B,都通过 _instance == 0 的判断,那么线程 A 和 B 都会创建新实例。单例模式保证生成唯一实例的规则被打破了。

双检测锁模式(Double-Checked Locking Pattern)

Lazy Singleton的一种线程安全改造是在 Instance() 中每次判断是否为 NULL(0) 前加锁,但是加锁是很慢的。而实际上只有第一次实例创建时的 _instance==NULL 判断才需要加锁,创建之后 _instance==NULL 判断不再需要加锁。所以多在加锁之前多加一层判断,需要判断两次所有叫 Double-Checked。

1
2
3
4
5
6
7
8
9
10
11
12
13
static Singleton *Instance()
{
if (_instance == NULL )
{
Lock(); // C++没有直接的Lock操作,请使用其它库的Lock,比如Boost,此处仅为了说明
if (_instance == NULL )
{
_instance = new Singleton ();
}
UnLock(); // C++没有直接的Lock操作,请使用其它库的Lock,比如Boost,此处仅为了说明
}
return _instance;
}

理论上问题解决了,但是在实践中有很多坑,如指令重排、多核处理器等问题让 DCLP 实现起来比较复杂,比如需要使用内存屏障,详细的分析可以阅读:

扩展:在C++11中有全新的内存模型和原子库,可以很方便的用来实现 DCLP。这里不展开。有兴趣可以阅读:

C++11 的并发特性提供了很多语法来实现线程安全的单例模式,其中值得推荐的有两种(在 最简洁的单例模式 有整理):

  • 使用 call_once
  • 使用 static local variable,即下文提到 Meyers Singleton

Eager Singleton

摘自 单例模式(Singleton)及其C++实现

这种实现在程序开始的时候(静态属性 instance 初始化)就完成了实例的创建。这正好和上述的 Lazy Singleton 相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//头文件中
class Singleton
{
public:
static Singleton& Instance()
{
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文件和其包含的头文件)中的初始化顺序是未定义的。如果在初始化完成之前调用 Instance() 方法会返回一个未定义的实例,比如一个全局变量的构造函数中调用了此方法。关于此问题在 C++ Singleton (单例) 模式最优实现 中有较为详细的分析。

Meyers Singleton

Scott Meyers 在《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用 local static 对象(函数内的 static 对象)。当第一次访问 Instance() 方法时才创建实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton  
{
public:
static Singleton& Instance()
{
static Singleton instance;
return instance;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
Singleton(Singleton&&);
Singleton& operator=(Singleton&&);
};

C++0X 以后,要求编译器保证内部静态变量 初始化 的线程安全,所以C++0x之后该实现是线程安全的is-meyers-implementation-of-the-singleton-pattern-thread-safe

返回引用在大多数场景中都是建议的。但返回指针的方式并不是一无是处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton
{
public:
static Singleton *GetInstance()
{
static Singleton m_Instance;
return &m_Instance;
}

private:
Singleton();
~Singleton(); // 避免用户 delete 指针
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
Singleton(Singleton&&);
Singleton& operator=(Singleton&&);
};

在 DLL 中使用单例模式时,主函数结束后析构全局变量或 static 变量会出现问题(比如线程无法退出)。此时提供 shutdown() 供用户显式调用,并不比直接暴露指针让用户 delete sington_ptr 高明。

无论是返回引用,还是返回指针都需要确保:

  1. 构造函数、析构函数 private
  2. 拷贝构造函数 = delete; 拷贝赋值操作符 = delete
  3. 移动构造函数 = delete; 移动赋值操作符 = delete

实例销毁

在上述几种实现中,大多使用了 new 操作符实例化对象。我们一般的编程观念是,new 操作符是需要和 delete 操作进行匹配的——这种观念是正确的。接下来我们说一个例外。

针对单例模式中的唯一实例,在实际项目中,特别是客户端开发,其实是不在乎这个实例的销毁的。因为,全局就这么一个变量,全局都要用,它的生命周期伴随着软件的生命周期,软件结束了,它也就自然而然的结束了,因为一个程序关闭之后,它会释放它占用的内存资源的,所以,也就没有所谓的内存泄漏了。但针对这种特殊性也有例外的地方:

在类中,有一些文件锁,文件句柄,数据库连接等等。相对应的,在类的析构行为中有必须的操作,比如关闭文件,关闭数据库连接等。如果 new 之后不进行显式的 delete(执行析构),就会造成潜在威胁,比如数据库连接数持续增加。

最直接的方法就是在程序结束时调用 Instance(),并对返回的指针调用 delete 操作。或者将 delete 的操作封装在 Singleton 类中,然后在程序结束时调用这个类方法。

1
2
3
4
5
6
7
8
static void DestoryInstance()
{
if (_instance != NULL )
{
delete _instance;
_instance = NULL ;
}
}

这样做可以实现功能,但也很丑陋,而且容易出错。因为这样的附加代码很容易被忘记,而且也很难保证在 delete 之后,没有代码再调用 Instance() 函数。

垃圾工人 RAII

一个妥善的方法是让这个类自己知道在合适的时候把自己删除,或者说把删除自己的操作挂在操作系统中的某个合适的点上,使其在恰当的时候被自动执行。

程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样(静态变量和全局变量在内存中,都是存储在静态存储区的,所以在析构时,是同等对待的)。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。

如下面的代码中的 CGarbo 类(Garbo 意为垃圾工人):

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
class Singleton  
{
private:
Singleton()
{
}
static Singleton *_instance;

class Garbo //它的唯一工作就是在析构函数中删除CSingleton的实例
{
public:
~Garbo()
{
if(Singleton::_instance)
delete Singleton::_instance;
}
};
static Garbo _garbo; //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数

public:
static Singleton * Instance()
{
if(_instance == NULL) //判断是否第一次调用
_instance = new Singleton();
return _instance;
}
};

//实现文件中
Singleton::Garbo Singleton::_garbo; //初始化

此时我们不再需要用户销毁实例(避免用户 delete 实例带来的不确定性),应该将析构函数声明为私有的。

Meyers Singleton

如果没有 new,当然也就不需要 delete

对于 2.3 节 Meyers Singleton 实现,其 Singleton::Instance() 函数中的局部静态变量会自动析构,所以 Singleton 类的析构函数会顺利执行。

不同类型的单例 vs. 多个单例

实现方法:继承,或模板

构造函数私有就无法继承了。如果有继承的需求,还是改为 protected 修饰。

如果是相关的单例类,比如 GoF 原著中的正常迷宫工厂、魔法迷宫工厂、炸弹迷宫工厂,可以通过继承父类的方式创建单例。因为从业务逻辑上讲,这三种工厂是“互斥”的,并不会同时存在,而单例类(父类及其延伸出来的子类)保证唯一实例,无法在一个系统中创建一个正常工厂实例和一个魔法工厂实例。

如果是不相关的单例类,比如系统中存在数据库连接类、配置管理类,此时是要求有各自的唯一的实例的,通过继承实现显然是不合适的(甚至是不可能的)。而这种场景恰恰是经常碰到的,如果每次都要重新写一次单例类的逻辑是很麻烦的。

如何使用模板实现一个通用的基类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class S>
class Singleton
{
public:
static S& GetInstance()
{
static S instance;
return instance;
};
private:
Singleton(){};
Singleton(const Singleton & ){};
Singleton & operator = ( const Singleton & rhs ){};
};

class DBOper : public Sington<DBOper>
{
friend class Sington<DBOper>;
//...
}

boost 中的单例模式

boost 中的单例模式

关于Boost Singleton do_nothing() 的那点事

参考来源

第二章多线程安全部分主要参考自 单例模式(Singleton)及其C++实现;第三章实例销毁参考自 C++设计模式——单例模式。当然,最主要的参考还是 GoF 的经典著作《设计模式:可复用面向对象软件的基础》。

这里再重复一次,在书中提到了“单件注册表方法”,此方法与上述所有实现最大的一个区别在于:Singleton 类不再负责创建单件实例,而上述实现无论返回单件指针还是引用都是在 Singleton 类中创建。