最近在学习设计模式,并决定把学习的东西写成笔记,也算是心得了吧。
——前言
定义:单例模式就是当你希望在你的工程中有个只会实例化一次的类(或者说只有一个对象)时,需要使用的设计模式。既然使用这种模式的类只可能有一个对象,自然要提供一个随时访问的全局访问点。
用更专业点的话说,单例模式确保类仅有一个实例,并该类提供了一个全局访问点。
实现:
单线程
对于一个单线程的程序来说,实现单例模式是很容易的,实现的技巧就是,将构造器设置为私有的(没学单例之前我都不知道可以这个样子),这样该类就只能在本类中实例化。再定义一个静态方法允许在其他类中获取该实例。而根据实例化的方式,可以分为两种,主动实例化和被动实例化(对应的也称之为饿汉式和懒汉式,个人更喜欢前面那种的名字);
主动实例化(饿汉式):在类被加载时就将自己实例化。为静态实例化方式。它在静态初始化期间或在类的构造器中分配变量。类一加载就实例化,所以要提前占用系统资源。当这个类初始化要占用很多资源时,这么写就有点囧了。java源码:
publicclass Singleton {
//唯一私有静态引用,类一加载就实例化
privatestatic Singleton uniqueInstance =new Singleton();
/**
* 注意这是一个私有构造器 ,也就是说单例模式只能自己实例化自己
*/
privateSingleton() {
//初始化
}
//提供了一个全局访问点
publicstatic Singleton instance() {
returnuniqueInstance;
}
}
被动实例化(懒汉式):在第一次调用instance()方法时,对该类进行实例化。java源码:
public class Singleton {
// 唯一私有静态引用
private static Singleton uniqueInstance =null;
/**
* 注意这是一个私有构造器 ,也就是说单例模式只能自己实例化自己
*/
private Singleton() {
//初始化
}
//提供了一个全局访问点,在多线程中,需要加一个关键字:synchronized
public static Singleton instance() {
if(uniqueInstance == null)
uniqueInstance = new Singleton();
return uniqueInstance;
}
}
两种实例化方式的比较:
相比较而言,主动实例化(饿汉式)比较占用资源,但是由于加载时即初始化,性能会较被动实例化(饿汉式)要好一些。在多线程的环境下,主动实例化不会有线程安全问题,被动实例化(饿汉式),初始过程中,当多个线程同时访问时,则有可能获得多个实例。
多线程
上述被动实例化的源代码,多线程中可能会有多个实例的问题,在java语言中,我们可以加一个同步关键字synchronized解决,但是性能上,就会差很多。因为只有在实例化的时候才需要同步,在实例化之后,这个同步就成为了无用的且昂贵的开销。对此,有些人提出了在c++语言中常用的双重上锁成例来解决。
下面先解释一下c++中的双重上锁成例,源代码:
class Singleton:
{
public:
//全局访问点,使用双重上锁成例(个人认为应该叫做双重判断)
static CSingleton * GetInstance()
{
if (m_pInstance == NULL)
{
①Lock();//伪代码,我并不知道在c++中如何上锁
②if(m_pInstance == NULL)
{
m_pInstance = new CSingleton();
}
return m_pInstance;
}
}
private:
CSingleton(//初始化){}; //与java相同的私有构造函数
static CSingleton * m_pInstance;
class CGarbo // 它的唯一工作就是在析构函数中删除CSingleton的实例
{
public:
~CGarbo()
{
if (CSingleton::m_pInstance)
delete CSingleton::m_pInstance;
}
};
static CGarbo Garbo; // 定义一个静态成员,在程序结束时,系统会调用它的析构函数
}
很遗憾,我并未在c++编程涉及过多线程,也未涉及到如何上锁,所以上述只能用伪代码写上锁了。下面解释一下为什么要双重上锁,和这个单例类的CGarbo内部类是做什么的。
双重上锁:
很显然,在多线程的情况下,可能有多个线程同时到达代码①处,之后上锁。如果没有代码②处的判断,那么多个线程依然会逐次的生成多个实例。反之,加上判断之后,第二个及以后的线程在代码②处判断为假,直接返回已有的对象。
C++的内存回收:
C++中内存需要自己管理,那么这个私有静态变量m_pInstance指向的空间该如何释放?!
放到析构函数里是不成立的。
问:m_pInstance指针指向的对象什么时候会调用析构函数?
答:在我执行delete m_pInstance的时候。。。那我都显式调用了,自然会释放资源,要析构函数做什么?!(当然,如果该单例中还有其他资源需要释放的话,析构函数还是必要的),这种额外的显示调用容易被人遗忘,更可怕的是,如果有其他线程需要使用这个对象,让你给提前释放了。。。那就是个大的bug.
有人用了特别巧妙的方法,让让这个类自己知道在合适的时候把自己删除。C++中,程序在结束的时候,系统会自动析构所有的全局变量和类的静态成员变量。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例,即也就是上面我们看到的CGarbo内部类,(Garbo意为垃圾工人)。
遗憾的是,由于java语言规范与c++不同,java中,双重上锁成例不可实现,具体是由于什么java允许无序写入(我是没太弄懂),具体解释见如下网址:
中文:http://www.ibm.com/developerworks/cn/java/j-dcl.html
英文:http://www.ibm.com/developerworks/java/library/j-dcl/index.html
需要指出的是,主动实例化(饿汉式)在c++中比较难实现,因为静态初始化在c++中没有固定的顺序,因而静态的uniqueInstance变量的初始化与类的加载顺序没有保证,可能会出问题。所以在c++中经常使用被动实例化。而java,则常常使用主动实例化。
总结:
我们看到,如果不是在多线程中,单例模式可以说是异常的简单(多线程也不难),多线程的话,java建议使用主动实例化(饿汉式),被动(懒汉式)的话,要加同步关键字。C++中,多使用被动实例化。在c++中,很多人使用这种方法 http://blog.csdn.net/aaa20090987/article/details/7385081 来实现,好像是很好的方法。
什么时候使用单例模式?
单例模式不难,不代表它容易使用。关键是什么时候使用单例模式。以个人的理解,那些你觉得在工程中需要由“一个人”完成的工作,多使用单例模式。举一个网上常见的例子:一台计算机可以有多台打印机,而这个计算机上又可能有多个进程(或线程)想要使用打印机。这时你就希望有这么“一个人”,来统一的管理打印机,而不会出乱子,这就可以使用单例模式。同理,我们的程序中的配置类也通常是单例模式的,资源管理也通常是单例模式。网上说单例模式还可以作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信(我觉得这个比较牵强)。
当然,还有其他你觉得符合单例模式好处的场合。
单例模式的好处:
控制只有一个实例,节省资源;
单例模式要注意:
1.多线程时,除实例化外,要注意写操作的线程安全。
2.据说不可以使用反射机制
3.据说也不可以序列化