单例模式的定义
定义 : 确保某一个类只有一个实例 , 而且自行实例化并向整个系统提供这个实例 .
单例模式应该是23种设计模式中最简单的一种模式了 . 它有以下几个要素:
- 私有的构造方法
- 指向自己实例的私有静态引用
- 以自己实例为返回值的静态的公有的方法
UML图 :
单例模式的分类
单例模式根据实例化对象时机的不同分为两种:一种是饿汉式单例模式,一种是懒汉式单例模式 .
饿汉单例模式
定义 : 即类产生的时候就创建好实例对象,这是一种空间换时间的方式;
代码如下:
//饿汉单例模式
#if 0
class Singleton
{
public:
//获取唯一可用的对象
static Singleton* getInstance()
{
return &singleton;
}
//类中其他方法 , 尽量是static
static void doSomething()
{
cout<<"我就是皇帝某某某....."<<endl;
}
private :
//单例对象在这里
static Singleton singleton ;
//让构造函数为 private,这样该类就不会被实例化 , 限制产生多个对象
Singleton()
{cout<<"单例对象创建"<<endl;}
~Singleton()
{cout<<"单例对象销毁"<<endl;}
};
//静态数据成员初始化 , 分配内存
//<数据类型><类名>::<静态数据成员名>=<值>
Singleton Singleton::singleton ;
int main()
{
Singleton *p1 = Singleton::getInstance();
Singleton *p2 = Singleton::getInstance();
Singleton *p3 = Singleton::getInstance();
//只有一个地址
cout<<"p1: "<<p1<<endl<<"p2: "<<p2<<endl<<"p3: "<<p3<<endl;
return 0;
}
#endif
运行结果:
饿汉单例模式是线程安全的,不需要考虑线程同步!
懒汉单例模式
定义 : 即在需要的时候,才创建对象,这是一种时间换空间的方式 ;
注 : 这是一种线程不安全的单例 . 我们后面来说.
代码如下 :
//懒汉单例模式
#if 0
//线程不安全的单例
class Singleton
{
public:
//注意这里和饿汉模式不同
static Singleton* getInstance()
{
if(singleton == nullptr)
{
singleton = new Singleton();
}
return singleton;
}
static void doSomething()
{
cout<<"我就是皇帝某某某....."<<endl;
}
private :
//注意这里 , 和饿汉模式声明的成员不同
static Singleton* singleton ;
Singleton()
{cout<<"单例对象创建"<<endl;}
~Singleton()
{cout<<"单例对象销毁"<<endl;}
//定义一个内部类
//如果单例模式的类中申请了其他资源,就无法释放,导致内存泄漏!
//原因:此时全局数据区中,存储的并不是一个实例对象,而是一个实例对象的指针,即一个地址变量而已!
//实例对象呢?在堆区,因为是通过new得来的!虽然这样能够减小全局数据区的占用,把实例对象这一大坨都放到堆区。
//可是!如何释放资源呢?
//定义内部类
class CGarbo
{
public:
CGarbo()
{}
~CGarbo()
{
if(singleton != nullptr)
{
delete singleton;
singleton = nullptr;
}
}
};
// 定义一个内部类的静态对象
// 当该对象销毁时,顺带就释放singleton指向的堆区资源
static CGarbo m_garbo;
};
Singleton* Singleton::singleton = nullptr;
Singleton::CGarbo Singleton::m_garbo;
int main()
{
Singleton *p1 = Singleton::getInstance();
Singleton *p2 = Singleton::getInstance();
Singleton *p3 = Singleton::getInstance();
cout<<"p1: "<<p1<<endl<<"p2: "<<p2<<endl<<"p3: "<<p3<<endl;
return 0;
}
#endif
运行结果:
在这里解释一下为什么使用内部类 , 这是因为如果在类A(如 : Singleton)的析构函数中delete 类A的对象 , delete 还会调用类A的析构函数 , 这样就造成了一个死循环 . 所以要采用内部类的方式进行析构 , 防止内存的泄漏 .
饿汉单例模式也可以写成这种形式 , 将static Singleton singleton ; 改为 static Singleton* singleton ; , 这里也需要使用内部类 .初始化的方式和懒汉单例模式一样.
这是一种线程不安全的单例
我们再来说说它为什么是线程不安全的单例.
该单例在低并发的情况下尚不会出现问题 , 若系统压力增大 , 并发量增加时则可能在内存中出现多个实例 , 破坏了最初的预期 . 为什么会出现这种情况呢?如一个线程A执行到 singleton = new Singleton(); , 但还没有获得对象(对象的初始化是需要时间的) , 第二个线程B也在执行 , 执行到if(singleton == nullptr) 判断 , 那么线程B获得的判断条件也是为真 , 于是继续运行下去 , 线程A获得了一个对象 , 线程B也获得了一个对象 , 在内存中就出现两个对象 !
解决方法
可以通过加锁的方式来解决.
static Singleton* getInstance()
{
lock(); //伪代码 , 具体实现请查阅资料
if(singleton == nullptr)
{
singleton = new Singleton();
}
unlock(); //伪代码
return singleton;
}
单例模式的应用
单例模式的优点
- 在内存中只有一个实例(对象) , 节省内存空间 .
- 避免频繁的创建销毁对象,可以提高性能 .
- 避免对共享资源的多重占用 .
- 可以全局访问 .
单例模式的缺点
- 单例模式一般没有接口 , 扩展很困难 .
- 单例模式对测试是不利的 .
- 单例模式与单一职责原则有冲突 .
单例模式的使用场景
- 需要频繁实例化然后销毁的对象 .
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象 .
- 在整个项目中需要一个共享访问点或共享数据 .
- 频繁访问数据库或文件的对象 .
- 要求生成唯一序列号的环境 .
- 需要定义大量的静态常量和静态方法(如工具类)的环境 .
单例模式的注意事项
- 在高并发的情况下 , 注意单例模式的线程同步问题 .
- 只能使用单例类提供的方法得到单例对象,不要使用反射,否则将会实例化一个新对象 .
- 不要做断开单例类对象与类中静态引用的危险操作 .
单例模式的扩展
如果一个类可以产生多个对象,对象的数量不受限制,则是非常容易实现的,直接使用new关键字就可以了,如果只需要一个对象,使用单例模式就可以了,但是如果要求一个类只
能产生两三个对象呢?该怎么实现?
这种需要产生固定数量对象的模式就叫做有上限的多例模式,它是单例模式的一种扩展,采用有上限的多例模式,我们可以在设计时决定在内存中有多少个实例 , 便系统进行扩展 , 修正单例可能存在的性能问题,提供系统的响应速度 .例如读取文件 , 我们可以在系统启动时完成初始化工作 , 在内存中启动固定数量的reader实例 , 然后在需要读取文件时就可以快速相应 .