全局变量、non-local static变量(文件域的静态变量和类的静态成员变量)在main执行之前就已分配内存并初始化;
local static 变量(局部静态变量)同样是在main前就已分配内存,第一次使用时初始化。
这里的变量包含内置数据类型和自定义类型的对象。
非局部静态变量一般在main执行之前的静态初始化过程中分配内存并初始化,可以认为是线程安全的;
局部静态变量在编译时,编译器的实现一般是在初始化语句之前设置一个局部静态变量的标识来判断是否已经初始化,运行的时候每次进行判断,如果需要初始化则执行初始化操作,否则不执行。这个过程本身不是线程安全的。C++0x之后该实现是线程安全的。
一个类只能被实例化一次,并提供一个访问它的全局访问点。
实现:
构造函数声明为private或protect防止被外部函数实例化,内部保存一个private static的类指针保存唯一的实例,实例的动作由一个public的类方法代劳,该方法也返回单例类唯一的实例。
懒汉实现:(线程不安全)
class singleton
{
private:
singleton(){}//构造函数为private,不能被用户访问,即用户不能new出来一个对象
static singleton* p;//static指针
public:
static singleton* instance();//此方法是获得本类实例的唯一全局访问点
};
singleton* singleton::p = NULL;
singleton* singleton::instance()
{
if (p == NULL)//若实例不存在,就new一个新的出来,否则,直接返回已有的实例
p = new singleton(); return p;
}
该方法是线程不安全的,考虑两个线程同时首次调用instance方法且同时检测到p是NULL值,则两个线程会同时构造一个实例给p,这是严重的错误!同时,这也不是单例的唯一实现!
单例大约有两种实现方法:懒汉与饿汉。
-
- 懒汉:故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化,所以上边的经典方法被归为懒汉实现;需要作双重锁定的处理才能保证多线程安全。
- 饿汉:饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化。静态初始化,类一加载就实例化。会提前占用系统资源。
特点与选择:
-
- 由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。
- 在访问量较小时,采用懒汉实现。这是以时间换空间。
懒汉式单例模式,为了保证线程安全,需要加锁,lock
方法1:加锁的经典懒汉实现:双重锁定
//头文件
class Singleton
{
public:
static Singleton& Instance() //Instance()作为静态成员函数提供里全局访问点
{
if(m_instance== NULL)//先判断实例是否存在,不存在再加锁,避免多次加锁与解锁操作
{
Lock(); //上锁
if(m_instance== NULL) //如果还未实例化,即可实例话,反之提供实例的引用
m_instance = new Singleton();
Unlock(); //解锁
}
return *m_instance; //返回指针的话可能会误被 delete,返回引用安全一点
}
private:
Singleton(); //这里将构造,析构,拷贝构造,赋值函数设为私有,杜绝了生成新例
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* m_instance;
};//源文件
Singleton* Singleton::ps = NULL;
双重锁定,是为了不用让线程每次都加锁,而只是在实例未被创建时再加锁处理,同时也能保证多线程的安全。而两次判空语句,即在lock里面又判断了一次实例是否存在,是因为:当同时有两个线程调用Instance()方法时,它们都可以通过第一重的判断m_instance==NULL的判断。然后由于lock机制,俩线程只有一个进入,另一个排队,必须其中一个进入并出来后另一个才可进入,此时若没有第二重的m_instance==NULL判断,则第一个线程创建了实例,第二个线程可以继续再创建实例,这就没达到单例的目的。
方法2:内部静态变量的懒汉实现
此方法也很容易实现,在instance函数里定义一个静态的实例,也可以保证拥有唯一实例,在返回时只需要返回其指针就可以了
class singleton
{
protected:
singleton()
{
pthread_mutex_init(&mutex);
}
public:
static pthread_mutex_t mutex;
static singleton* initance();
int a;
};
pthread_mutex_t singleton::mutex;
singleton* singleton::initance()
{
pthread_mutex_lock(&mutex);
static singleton obj;//静态对象
pthread_mutex_unlock(&mutex);
return &obj;
}
把静态对象放到函数里边,省的在外面声明,只要返回一个静态类的地址,就算这个函数执行完也不会被销毁,它被保存在静态区,和全局变量差不多。
饿汉式单例模式:
饿汉实现本来就是线程安全的,不用加锁
class singleton
{
protected:
singleton(){}
private:
static singleton* instance;//static的,类一加载,就会初始化
public:
static singleton* initance();
};
singleton* singleton::instance = new singleton();
singleton* singleton::initance()
{
return instance;
}
非局部静态变量,即类中的静态成员变量instance,在main执行前就分配内存并初始化,是线程安全的。但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元(可理解为cpp文件和其包含的头文件)中的初始化顺序是未定义的。如果在初始化完成之前调用 Instance()方法会返回一个未定义的实例。例如有两个单例 SingletonA 和 SingletonB ,都采用了 Eager Initialization ,那么如果 SingletonA 的初始化需要 SingletonB ,而这两个单例又在不同的编译单元,初始化顺序是不定的,如果 SingletonA 在 SingletonB 之前初始化,就会出错。
应用:
如果系统有类似的实体(有且只有一个,且需要全局访问),那么就可以将其实现为一个单例。实际工作中常见的应用举例:
1)日志类,一个应用往往只对应一个日志实例。
2)配置类,应用的配置集中管理,并提供全局访问。
3)管理器,比如windows系统的任务管理器就是一个例子,总是只有一个管理器的实例。
4)共享资源类,加载资源需要较长时间,使用单例可以避免重复加载资源,并被多个地方共享访问。
单例模式常常与工厂模式结合使用,因为工厂只需要创建产品实例就可以了,在多线程的环境下也不会造成任何的冲突,因此只需要一个工厂实例就可以了。
先看一个最简单的教科书式单例模式:
class CSingleton
{
public:
static CSingleton* getInstance()
{
if (NULL == ps)
{//tag1
ps = new CSingleton;
}
return ps;
}
private:
CSingleton(){}
CSingleton & operator=(const CSingleton &s);
static CSingleton* ps;
};
CSingleton* CSingleton::ps = NULL;
有2个要点:
1.private的构造函数和=操作符,用于防止类外的实例化和被复制;
2.static的类指针和get方法。
在大多数单线程情况下,以上代码大都会运行得很好,除非遇到中断:
1.当程序运行到tag1 处触发了中断;
2.中断处理程序恰调用的也是getInstance函数。
可想而知,这和多线程的情况类似,假设线程A 运行到tag1处,还没来得及new,此时ps仍然是NULL,线程B(或中断处理程序) 同时也运行到此通过if判断,那么将会实例化2个CSingleton对象,显然是不对的。
为了解决上述问题,自然而然,最容易想到也最常用的方法是加锁,因此getInstance改成这样:
static CSingleton* getInstance()
{
lock();//伪代码
if (NULL == ps)
{
ps = new CSingleton;
}
return ps;
}
加了锁以后貌似解决了上述问题,但也同样带来了新的问题:如果程序到处是诸如:
CSingleton::instance()->aaaa();
CSingleton::instance()->bbbb();
CSingleton::instance()->cccc();
这样的调用,除了第一次的lock()有用外,后面的都是在做无用功,lock()的代价说大不大,但在某些情况下还是会提高程序延迟,这对追求完美的程序猿来说是完全无法接受的。
于是乎,咱想出了一个办法:
static CSingleton* getInstance()
{
if (NULL == ps)//这里加了次判断,只有第一次才会为true而调用lock()
{
lock();//伪代码
if (NULL == ps)
{
ps = new CSingleton;
}
}
return ps;
}
很久以后我才知道,这个方法有个很高大上的名字,叫做双重检查锁定模式,简称DCLP(Double Checked Locking Pattern)。
DCLP很好地解决了多次调用不必要的lock()。
然而,你们以为这样就完了?too young。。
DCLP在多线程下仍然存在2个根本问题:
1.程序的指令执行顺序不确定;
2.编译器优化问题。
先说2,在某些编译器下,以上的两个if判断只会执行一个,甚至一个都不执行,原因是编译器认为至少有一个if判断是多余的,它自动帮助我们优化了代码。
再说1,ps = new CSingleton; 这条语句会被拆分为这样的三个步骤执行:
1.为要new的对象开辟一块内存;
2.构造该对象,填入这块内存;
3.将ps指针指向这块内存。
以上三个步骤,2和3的顺序是不确定的,可能先2后3,也可能先3后2。。。
实际执行时可能是这样的:
static CSingleton* getInstance()
{
if (NULL == ps)
{
lock();//伪代码
if (NULL == ps)
{ //伪代码
ps = xx;//step 3
new sizeof(CSingleton);//step 1
new CSingleton;//step 2
}
}
return ps;
}
如果编译器按上述顺序执行代码,考虑如下状况:
线程A 执行到step 1还未执行后面的step 2,此时ps非空,但其指向的内存里面的内容还未被构造出来,于此同时线程B 进入这个函数,判断ps非空直接返回ps,但是调用者此时访问的ps内存实际内容CSingleton还没被构造呢,这是一块地址正确大小正确但内部数据不明的东西,当然会出错(调用者一般这么调用:CSingleton::getInstance()->aa(); CSingleton::getInstance()->bb(); CSingleton::getInstance()->cc();........此时的aa,bb,cc是啥玩意儿?)。
这也是为什么加上volatile关键字仍然不可以解决同步问题,volatile只解决了编译器优化问题,却无法控制机器指令执行顺序。
很遗憾的是,C/C++本身在设计时是不考虑多线程问题的,也就是说,要处理多线程问题还要程序猿自己想办法填坑。。
说了这么多,我们要讨论的问题仍然没有解决,庆幸的是,C++ 11提供了内存栅栏技术来解决这个问题,这里不赘述,有兴趣的读者可以自己搜索资料看看,不过是一些api调罢了。
那么,C++ 11 以前的代码如何解决这个问题呢?很不幸,并没有很好的解决方案,一种可行的方案是,程序中不要到处这么调用这个单例对象:
CSingleton::getInstance()->aa();
CSingleton::getInstance()->bb();
CSingleton::getInstance()->cc();
而是在程序开始就初始化缓存这个单例对象:
CSingleton* const g_ps = CSingleton::getInstance();//程序一开始就缓存这个单例对象
g_ps->aa();
g_ps->bb();
g_ps->cc();
但是如此带来的问题是程序一开始就实例化了这个单例对象,对象在整个程序的声明周期存在,这貌似叫饿汉式,而之前那种叫懒汉式,孰轻孰重,只有根据实际情况取舍了。