一、什么是单例模式
属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
二、适用场景和实现要点
1、适用场景
- 需要生成唯一序列的环境(频繁访问数据库或文件)。
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 方便资源相互通信的环境
1.1 单例模式经典使用场景
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
- 控制资源的情况下,方便资源之间的互相通信。如线程池等。
1.2 应用场景举例
- 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件 。
- Windows的Task Manager(任务管理器)就是很典型的单例模式。
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.
2、实现要点
- 将该类的,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
- ;
- 在该类内提供一个,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;
- 提供一个静态的公有的函数用于创建或获取它本身的静态私有对象。
三、C++实现单例的几种方式
1、有缺陷的懒汉式
懒汉式(Lazy-Initialization)的方法是,也就说直到调用getInstance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存。
// 有如下问题:
// 1.线程不安全
// 2.内存泄漏
class Singleton
{
private:
Singleton()
{
std::cout<<"创建构造函数!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton* m_pInstance;
public:
~Singleton()
{
std::cout<<"析构函数!"<<std::endl;
}
static Singleton* getInstance()
{
if (m_pInstance == nullptr)
{
m_pInstance = new Singleton;
}
return m_pInstance ;
}
};
Singleton* Singleton::m_instance_ptr = nullptr;
int main()
{
Singleton* instance = Singleton::getInstance();
Singleton* instance_2 = Singleton::getInstance();
return 0;
}
运行结果是
创建构造函数!
可以看到,获取了两次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现,同时也存在一些问题
- 线程安全问题,当多线程获取单例时有可能引发竞态条件:
第一个线程在if中判断m_pInstance 为空,实例化单例对象;同时第二个线程在获取单例时也判断m_pInstance 为空,实例化单例对象,会实例化两个对象。- 内存泄漏,注意到类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。
2、线程安全且内存安全的懒汉式单例 (智能指针,锁)
class Singleton
{
public:
typedef std::shared_ptr<Singleton> Ptr;
~Singleton()
{
std::cout<<"析构函数!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Ptr getInstance()
{
// 双检锁
if (m_pInstance == nullptr)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_pInstance == nullptr)
{
m_pInstance = std::shared_ptr<Singleton>(new Singleton);
}
}
return m_pInstance ;
}
private:
Singleton()
{
std::cout<<"创建构造函数!"<<std::endl;
}
static Ptr m_pInstance;
static std::mutex m_mutex;
};
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;
int main()
{
Singleton* instance = Singleton::getInstance();
Singleton* instance_2 = Singleton::getInstance();
return 0;
}
运行结果如下,发现确实只构造了一次实例,并且发生了析构。
创建构造函数!
析构函数!
shared_ptr和mutex都是C++11的标准,以上这种方法的优点是
- 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
- 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 getInstance的方法都加锁,锁的开销毕竟还是有点大的。
不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。还有更为严重的问题,在某些平台(与编译器和指令集架构有关),双检锁会失效
!
3、最推荐的懒汉式单例(magic static )——局部静态变量
class Singleton
{
public:
~Singleton()
{
std::cout<<"析构函数!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton()
{
std::cout<<"创建构造函数!"<<std::endl;
}
};
int main()
{
Singleton& instance = Singleton::getInstance();
Singleton& instance_2 = Singleton::getInstance();
return 0;
}
运行结果
创建构造函数!
析构函数!
运用C++静态变量的特性,生存周期是从声明到程序结束。
四、单例模式优缺点
优点
- 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。
- 避免对共享资源的多重占用。
缺点
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。