一、单例设计模式概述
单例模式,也叫单子模式,是一种常用的软件设计模式,属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。----引述维基百科。
简单来说,单例设计模式的作用,就是希望设计一个类,这个类从始到终只允许被创建一个对象。
二、单例设计模式实现的过程
- 要求一个类只能被创建一个类对象,则在设计这个类的时候,需要把这个类的构造函数的控制起来,不让这个类的使用者通过构造函数来构造这个类对象,所以把这个的类的构造函数设置为private访问权限。
- 既然已经设计将类的构造函数设置为private,不让这个类的用户使用构造函数来创建类对象,则需要为这个类的用户提供一个返回这个类对象的方法。
- 返回类对象的方法,需要的满足多次调用这个方法的时候,从始到终返回的都是同一个类对象。
- 更进一步,就是要考虑类对象的析构(资源有没有被释放)、线程同步等问题了。
三、使用Java实现单例设计模式
- 饿汉式的单例设计模式
所谓的饿汉式,就是先把类对象创建出来,赋给一个类的私有成员变量。用java实现饿汉式的单例设计模式的代码如下:
class Singleton
{
private Singleton() {} //1.类构造函数私有
int flag = 0;
private static Singleton obj = new Singleton(); //私有成员变量,创建类对象
void setFlag(int n) {
flag = n;
}
int getFlag() {
return flag;
}
static Singleton getInstance() {//2.返回类对象的一个方法,由于static限定,可以通过类名调用这个函数
System.out.println("this is a singleton demo.");
return obj;
}
public static void main(String []args)
{
Singleton single = Singleton.getInstance();
single.setFlag(100);
Singleton single2 = Singleton.getInstance();
System.out.println("--------------\n" + single2.getFlag());
}
}
运行这个程序,得到的结果为:
打印single2对象的Flag的值,得到的是single对象设置的值,由此说明了single对象和single2对象实质是同一个对象。
进一步分析,由于饿汉式的单例设计模式中创建类对象private static Singleton obj = new Singleton();是一个原子操作,只能创建出一个类对象,所以饿汉式的单例设计模式是线程安全的,而且简单,所以在工作中更多是使用这个单例设计模式,缺点就是单例对象一直都存在内存中,占点内存。
- 懒汉式的单例设计模式
所谓的懒汉式,就是不到使用单例对象的时候,就不创建单例对象。
使用Java实现懒汉式设计模式的代码如下:
public class Singleton {
private static volatile Singleton obj = null;
// Private constructor suppresses
// default public constructor
private Singleton() {};
//Thread safe and performance promote
public static Singleton getInstance() {
if(obj == null){
synchronized(Singleton.class){
// When more than two threads run into the first null check same time,
// to avoid instanced more than one time, it needs to be checked again.
if(obj == null){
obj = new Singleton();
}
}
}
return obj;
}
}
使用双if来确保线程同步,创建出来的对象只有一个。这里使用了synchronized()函数来实现线程同步,需要付出的代价是多消耗系统资源。
四、使用C++实现单例设计模式
使用C++设计这样一个类如下:
#include <iostream>
// version1:
// with problems below:
// 1. thread is not safe
// 2. memory leak
class Singleton{
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton* m_instance_ptr;
public:
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
static Singleton* get_instance(){
if(m_instance_ptr==nullptr){
m_instance_ptr = new Singleton;
}
return m_instance_ptr;
}
void use() const { std::cout << "in use" << std::endl; }
};
Singleton* Singleton::m_instance_ptr = nullptr;
int main(){
Singleton* instance = Singleton::get_instance();
Singleton* instance_2 = Singleton::get_instance();
printf("instance pointer:%p,instance_2 pointer:%p\n", instance. instance_2);
return 0;
}
可以看到,两个指针的内容是一样,获取了两次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这个是C++最基础版本的单例实现,它有哪些问题呢?
- 线程安全的问题,当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance_ptr是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来;解决办法:加锁
- 内存泄漏. 注意到类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。解决办法: 使用共享指针;
因此,这里提供一个改进的,线程安全的、使用智能指针的实现;
#include <iostream>
#include <memory> // shared_ptr
#include <mutex> // mutex
// version 2:
// with problems below fixed:
// 1. thread is safe now
// 2. memory doesn't leak
class Singleton{
public:
typedef std::shared_ptr<Singleton> Ptr;
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Ptr get_instance(){
// "double checked lock"
if(m_instance_ptr==nullptr){
std::lock_guard<std::mutex> lk(m_mutex);
if(m_instance_ptr == nullptr){
m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
}
}
return m_instance_ptr;
}
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
static Ptr m_instance_ptr;
static std::mutex m_mutex;
};
// initialization static variables out of class
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;
int main(){
Singleton::Ptr instance = Singleton::get_instance();
Singleton::Ptr instance2 = Singleton::get_instance();
return 0;
}
shared_ptr和mutex都是C++11的标准,以上这种方法的优点是
- 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
- 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。
不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。
最推荐的懒汉式单例(magic static )——局部静态变量
#include <iostream>
class Singleton
{
public:
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton& get_instance(){
static Singleton instance;
return instance;
}
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
};
int main(int argc, char *argv[])
{
Singleton& instance_1 = Singleton::get_instance();
Singleton& instance_2 = Singleton::get_instance();
return 0;
}
这种方法又叫做 Meyers’ SingletonMeyer’s的单例, 是著名的写出《Effective C++》系列书籍的作者 Meyers 提出的。
这是最推荐的一种单例实现方式:
- 通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2013支持该特性);
- 不需要使用共享指针,代码简洁;
- 注意在使用的时候需要声明单例的引用 Single& 才能获取对象
五、总结
单例设计模式是工作中比较常用的一种设计模式,相对于其他的设计模式,单例设计模式还是简单的,但应用单例设计模式的时候,还是有一些容易出错的点需要注意的,比如饿汉式和懒汉式的优劣之处,线程同步怎样实现等。
参考:参考