之前看一个朋友在学Java,顺口问他,你会设计模式吗?
他说知道一点吧。
然后我就问,那你会几种单例模式的写法吗?
他说,你这个孔乙己。
首先澄清一下,这篇文章并不是“茴香豆的茴字有X种写法”,文章题目只是调侃一下而已。一个字有多种写法,它们之间的功能是一样的并列的,你只需要会一个最通用,大家都认得的字就可以了。
但是,线程安全和线程不安全的单例模式的写法,你能说他们仅仅只是不同的写法而已吗?功能都不一样对吧?
还有效率高低不同(比如暴力加锁就效率很低),也是不一样的写法吧?
最后还有易读性和易维护性的区别,这个见仁见智吧。
最后,再澄清一下,我并不怎么会Java,只是因为做过一点安卓,所以略知一二而已,并没有深入研究过。然后,本文的例子代码都使用C++。
加锁仿佛在需要同步互斥时,是最简单暴力的做法,但它在多线/进程的环境下的效率不高,不到万不得已,就不考虑它了。
我们一般都是这样写:
static TYPE getInstance() {
// ...
}
那么TYPE应该是什么?
一个对象副本?这违背了单例模式的初衷……并且如果复制构造函数代价太高的话,也很不好。
一个指针?可以!
说到指针,肯定就少不了引用了,引用也是可以的,并且引用更加自然(毕竟是直接通过对象的别名来直接操纵对象)。
这里的“格式”并不是说这种写法值得学习,是范例,而是给一个单例模式书写的框架,后续的各种改进版,都基于它的结构。注意,这里贴了Singleton类的声明和实现,后续的写法只贴getInstance函数的实现,而不再重复其它结构了,基本都一样的!
class Singleton
{
public:
static Singleton* getInstance();
private:
// 可以有很多类型的构造函数,这里只是简单举例
Singleton(const string& initialName);
// private + 只声明而不定义,从而可以禁止构造时的复制和赋值时的复制,保持单例
Singleton(const Singleton&);
Singleton& operator = (const Singleton&);
static Singleton* obj;
string name; // 非原始类型的数据成员,代表需要在程序结束时主动释放的资源
};
而cpp文件则这样实现:
#ifndef __SINGLETON_H__
#define __SINGLETON_H__
Singleton* Singleton::obj = NULL;
Singleton::Singleton(const string& initialName)
: name(initialName) {
cout << "Singleton create: " << name << endl;
}
Singleton* Singleton::getInstance() {
if (obj == NULL) {
obj = new Singleton("name given in code");
}
return obj;
}
#endif
这个意图应该是很明显的,因为我们只想外界通过getInstance来获取对象的实例,而不是能够使用构造函数来自定义产生对象。
这是阻止复制对象的做法!参考《Effective C++》第三版的条款6。我解释一下,声明为private,原因和上面相同,不让外部调用;而不实现,一方面是因为本身根本不会用到,没必要实现;另一方面,为了防止友元类或友元函数来调用!因为如果它们调用了,你没有实现函数体,那么就会在编译器的链接阶段报错,成功阻止!!!
如果不了解线程安全这个概念,可以读一下wiki上的定义:
线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。——来自维基百科
这种写法是:
Singleton* Singleton::getInstance() {
if (obj == NULL) {
obj = new Singleton("name given in code");
}
return obj;
}
这种写法为何不是线程安全的?
假设有两个线程A和B,它们的指令执行序列是这样的:
初始时:obj的值为NULL
A: if (obj == NULL) // 结果为真
B: if (obj == NULL) // 结果为真
A: obj = new Singleton("name given in code");
B: obj = new Singleton("name given in code");
...
这是什么结果?还是单例吗?内存泄露了知道不?(解释一下,第一个new出来的对象本来由obj指向,但obj被第二个new出来的指针覆盖了,所以没有任何指针指向第一个对象,它变成了一个“野对象”,没爹疼,没娘爱,真可怜)
有人看到上面的分析,就会想,既然是因为在运行的时候初始化,才造成可能存在的多个线程之间的竞争,那么如果在main函数开始执行之前就已经初始化好了,会怎么样呢?不就没有竞争了吗?比如:
Singleton* Singleton::obj = new Singleton("name given in code");
Singleton* Singleton::getInstance() {
return obj;
}
没错,这样“抢先初始化”的方法是对的,它确实会在main函数的主体执行之前执行!要知道,程序总是单线程启动的(这一点我没做过考证啊……so sad,不过按照常识,确实是这样的) ,在main函数开始执行之后,才有可能开始多线/进程工作。
但是,有没有发现,这种写法,需要你在代码里就确定好要给对象怎样构造,比如上面的例子就是在代码里给定了它的名字。那如果我这个名字,是得等到运行后,或者由用户输入,或者由其它条件给出的呢?
上一步的写法的缺点就是,初始化的时机太早了,还需要其它的外部条件才能够进行初始化,所以这里就:
// 贴一下完整的代码,下面要改版类声明了
#include
#include
using namespace std;
class Singleton
{
public:
static Singleton* getInstance();
private:
// 可以有很多类型的构造函数,这里只是简单举例
Singleton(const string& initialName);
// private+只声明而不定义,从而可以禁止构造时的复制和赋值时的复制,保持单例
Singleton(const Singleton&);
Singleton& operator = (const Singleton&);
static Singleton* obj;
string name; // 非原始类型的数据成员,代表需要在程序结束时主动释放的资源
};
Singleton::Singleton(const string& initialName)
: name(initialName) {
cout << "Singleton create: " << name << endl;
}
const string& getNameFromOuter() {
static string name;
cin >> name;
return name;
}
// 这是个不好的示例,因为使用了全局函数来给定类的名字,耦合性太高
Singleton* Singleton::obj = new Singleton(getNameFromOuter());;
Singleton* Singleton::getInstance() {
return obj;
}
int main() {
cout << "main function begin" << endl;
Singleton* obj1 = Singleton::getInstance();
Singleton* obj2 = Singleton::getInstance();
Singleton* obj3 = Singleton::getInstance();
return 0;
}
这里其实只是用函数来代替常量来做初始化而已,方法虽土,但是可行就好,更多分析请参考《Effective C++》第三版,条款4。
注意在程序退出的时候,系统会回收各种资源,但是如果我们想做一些自己的回收工作(其实就是执行自己定义的destructor),比如你有一些资源是动态申请的,需要及时归还,想做一些日志记录,等等。
上面的写法能够做到吗?上面都是用到指针,你觉得能做到吗?答案是否定的。
从开篇关于“返回值”的讨论,还剩下一个“引用”,试一下用引用,会怎么样?
// 由于这次的类声明已经改变很多了,所以贴了所有代码
#include
#include
using namespace std;
class Singleton
{
public:
static Singleton& getInstance();
private:
// 可以有很多类型的构造函数,这里只是简单举例
Singleton(const string& initialName);
// private+只声明而不定义,从而可以禁止构造时的复制和赋值时的复制,保持单例
Singleton(const Singleton&);
Singleton& operator = (const Singleton&);
string name; // 非原始类型的数据成员,代表需要在程序结束时主动释放的资源
};
Singleton::Singleton(const string& initialName)
: name(initialName) {
cout << "Singleton create: " << name << endl;
}
const string& getNameFromOuter() {
static string name;
cin >> name;
return name;
}
// 实质上是等到第一次调用getInstance才会执行constructor的!
Singleton& Singleton::getInstance() {
static Singleton obj(getNameFromOuter());
return obj;
}
int main() {
cout << "main function begin" << endl;
Singleton& obj1 = Singleton::getInstance();
Singleton& obj2 = Singleton::getInstance();
Singleton& obj3 = Singleton::getInstance();
return 0;
}
这样的写法,是等到第一次调用getInstance才会执行constructor的!
为什么和上面的不同,不是在main函数开始执行前初始化的吗?
A. 前面的本质是利用:C++对静态成员变量的初始化时机在main函数执行之前。
B. 但是这里没有静态成员变量了,换成一个静态成员函数里的一个局部静态变量,所以自然就是等到main函数开始执行后才会进行初始化的!
那么问题来了,假设在第一次调用getInstance之前,程序已经变成多线程的了,然后有两个线程A和B,它们“同时”调用getInstance(这里的同时不是真的同时,因为在同一个进程中的多个线程,同一时刻里,最多只有一个线程在执行指令,造成它们“同时”的假像是由于CPU的调度,参考最上面的例子),会发生什么事情呢?
这个涉及到C++编译器怎么处理函数内部的static变量的初始化了,说实话,我现在还不懂。
不过,从最坏的情况来看,即使会出现两次初始化,也并不会造成坏的影响(比如最前面说到的构建了两个对象,内存泄露等)。
为什么呢?因为static变量的存储区域在编译时就确定的了,即使有多次初始化,也都是在那片内存区域上进行的,不会有内存泄露的问题!
而重复初始化的话,其实也并没有差别,就是这样的过程:
int a = 1;
a = 1;
a = 1;
...
a的值始终是1,有坏的影响吗?
如果有,也就是重复构造的代价而已。
这种重复构造会很多次吗?
不会,因为两个线程“同时”进行初始化,本来就是概率很低的情况了,而一旦初始化过后,以后都不会再进行了,所以这样的开销(假设万一有)是小概率且一次性的。
不忘初心,当初想要这种写法,是出于在程序退出时能够自动释放资源的需求,那么这样写,真的能够吗?
毫无疑问啊,因为声明的是函数内部的局部静态变量,C++规定在程序退出时会自动析构它的!
其实上面的所有东西都是纸上谈兵而已,可以弄个多线程环境来验证吗?咕~~(╯﹏╰)b,我觉得不太现实,目前还没想到有方法可以构造出竞争条件(没法控制cpu调度啊)。
如果有朋友知道怎么验证的,请务必告诉我,真心好奇!
用单例模式写出来的类是没法继承的吗?
对啊,因为你的构造函数是private的。
真的没法继承吗?
可以啊,只需要把构造函数声明为protected而不是private就好了!
如果工程里用到单例模式的地方较多,可以考虑将其封装成为一个模板,然后通过实例化、继承,来实现代码重用。
首先在java里写单例模式,跟C++就很不一样了,因为语言特性狠不一样,我还接触过pyhton的写法,更加“奇葩”。
所以我觉得,对于单例模式,不应该纠结于怎么写,而更应该关注,为什么要这样写!
我写这篇文章也是一直按照“为什么要这样写”的思路来写的,分析了上一种的利弊,然后提出改进需求,最后实现完善。