最近写代码因为疏忽单例的判断,导致程序快速切换多次启动时出现了bug ,借这个机会梳理一下几种单例模式的构造方式。单例模式存在的意义是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
- 解决方法1(懒汉式)
一种实现方法是定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。单例模式通过类本身来管理其唯一实例,这种特性提供了解决问题的方法。唯一的实例是类的一个普通对象,但设计这个类时,让它只能创建一个实例并提供对此实例的全局访问。唯一实例类Singleton在静态成员函数中隐藏创建实例的操作。
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
static CSingleton *m_pInstance;
public:
static CSingleton * GetInstance()
{
if(m_pInstance == NULL) //判断是否第一次调用
m_pInstance = new CSingleton();
return m_pInstance;
}
};
用户访问唯一实例的方法只有GetInstance()成员函数。如果不通过这个函数,任何创建实例的尝试都将失败,因为类的构造函数是私有的。GetInstance()使用懒惰初始化,也就是说它的返回值是当这个函数首次被访问时被创建的。所有GetInstance()之后的调用都返回相同实例的指针。
上面定义的CSingleton单例类有以下三个特征:(1 具有一个指向唯一实例的私有静态指针m_pInstance; (2 有一个可以获取这个唯一实例的公有函数,并且在需要的时候创建该实例; (3 构造函数私有,外界不可创建该类实例;
这种实现方式存在的一个问题是m_pInstance变量的释放问题,即实例的析构问题,虽然我们可以在程序结束时主动调用GetInstance()方法并对其返回的指针智行delete操作,但是这种方式不仅繁琐,而且调用者忘记的话,会引起很多的问题。
对于上面这种问题的一种解决方法是在该类中定义一个静态全局变量,我们知道,程序在结束的时候,系统会自动析构所有的全局变量。系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。我们可以在CSingleton单例类中定义一个这样的静态成员变量,在它的析构函数中删除单例类的实例。
class CSingleton
{
private:
CSingleton()
{
}
static CSingleton *m_pInstance;
class CGarbo //它的唯一工作就是在析构函数中删除CSingleton的实例
{
public:
~CGarbo()
{
if(CSingleton::m_pInstance)
delete CSingleton::m_pInstance;
}
};
static CGarbo Garbo; //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数
public:
static CSingleton * GetInstance()
{
if(m_pInstance == NULL) //判断是否第一次调用
m_pInstance = new CSingleton();
return m_pInstance;
}
};
- 解决方法2(饿汉式)
如果你对上面添加在单例类内部添加一个类静态对象的方法不是很满意,还可以使用局部静态变量方法构造:
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
public:
static CSingleton & GetInstance()
{
static CSingleton instance; //局部静态变量
return instance;
}
};
用此种方法会出现类拷贝的问题,例如 Singleton singleton = Singleton :: GetInstance();这种调用方式编译器会为类生成一个默认的构造函数,来支持类的拷贝。这样的话就违背了单例的特性,所以我们可以稍微改一下,返回的是指针:
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
public:
static CSingleton * GetInstance()
{
static CSingleton instance; //局部静态变量
return &instance;
}
};
也可以不让编译器这么做,显示的声明类拷贝的构造函数,重载 = 操作符
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
CSingleton(const CSingleton &);
CSingleton & operator = (const CSingleton &);
public:
static CSingleton & GetInstance()
{
static CSingleton instance; //局部静态变量
return instance;
}
};
C++的单例模式与线程安全单例模式(懒汉/饿汉)
1 教科书里的单例模式
我们都很清楚一个简单的单例模式该怎样去实现:构造函数声明为private或protect防止被外部函数实例化,内部保存一个private static的类指针保存唯一的实例,实例的动作由一个public的类方法代劳,该方法也返回单例类唯一的实例。
上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class
singleton
{
protected
:
singleton(){}
private
:
static
singleton* p;
public
:
static
singleton* instance();
};
singleton* singleton::p = NULL;
singleton* singleton::instance()
{
if
(p == NULL)
p =
new
singleton();
return
p;
}
这是一个很棒的实现,简单易懂。但这是一个完美的实现吗?不!该方法是线程不安全的,考虑两个线程同时首次调用instance方法且同时检测到p是NULL值,则两个线程会同时构造一个实例给p,这是严重的错误!同时,这也不是单例的唯一实现!
2 懒汉与饿汉
单例大约有两种实现方法:懒汉与饿汉。
-
- 懒汉:故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化,所以上边的经典方法被归为懒汉实现;
- 饿汉:饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化。
特点与选择:
-
- 由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。
- 在访问量较小时,采用懒汉实现。这是以时间换空间。
3 线程安全的懒汉实现
线程不安全,怎么办呢?最直观的方法:加锁。
方法1:加锁的经典懒汉实现:
class singleton
{
protected:
singleton()
{
pthread_mutex_init(&mutex);
}
private:
static singleton* p;
public:
static pthread_mutex_t mutex;
static singleton* initance();
};
pthread_mutex_t singleton::mutex;
singleton* singleton::p = NULL;
singleton* singleton::initance()
{
if (p == NULL)
{
pthread_mutex_lock(&mutex);
if (p == NULL)
p = new singleton();
pthread_mutex_unlock(&mutex);
}
return p;
}
-
方法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;
}
4 饿汉实现
为什么我不讲“线程安全的饿汉实现”?因为饿汉实现本来就是线程安全的,不用加锁。为啥?自己想!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class
singleton
{
protected
:
singleton()
{}
private
:
static
singleton* p;
public
:
static
singleton* initance();
};
singleton* singleton::p =
new
singleton;
singleton* singleton::initance()
{
return
p;
}
是不是特别简单呢?
以空间换时间,你说简单不简单?
面试的时候,线程安全的单例模式怎么写?肯定怎么简单怎么写呀!饿汉模式反而最懒[正经脸]!
单例模式中,饿汉式和懒汉式有什么区别?各适合用在哪里?为什么说推荐用饿汉模式?
饿汉式:
public class Singleton{
private static Singleton singleton = new Singleton ();
private Singleton (){}
public static Singleton getInstance(){return singletion;}
}
懒汉式:
public class Singleton{
private static Singleton singleton = null;
public static synchronized synchronized getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
比较:
饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变
懒汉式如果在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的
推荐使用第一种
从实现方式来讲他们最大的区别就是懒汉式是延时加载,
他是在需要的时候才创建对象,而饿汉式在虚拟机启动的时候就会创建,
饿汉式无需关注多线程问题、写法简单明了、能用则用。但是它是加载类时创建实例(上面有个朋友写错了)、
所以如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用一块创建。
懒汉式的优点是延时加载、缺点是应该用同步(想改进的话现在还是不可能,比如double-check)、其实也可以不用同步、
看你的需求了,多创建一两个无引用的废对象其实也没什么大不了。