设计模式之单例模式(c++)

单例模式介绍


单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。

单例模式就是一个类只能被实例化一次 ,更准确的说是只能有一个实例化的对象的类,该实例被所有程序模块共享。

类似于全局变量。

有很多地方需要这样的功能模块,如系统的日志输出,GUI应用必须是单鼠标,MODEM的联接需要一条且只需要一条电话线,操作系统只能有一个窗口管理器,一台PC连一个键盘等。

使用场景:

  • 全进程仅需要唯一实例(对象)的情况

实现


通过以上分析,要实现单例模式,需要做到以下几点:

  • 要保证只能产生一个实例
  • 不能通过构造函数创建实例,因为如果可以,就可能产生多个实例

那么,单例类的构造函数就要是私有的,可以考虑如下步骤:

  • 类的构造函数是私有的,防止外部new实例化
  • 把复制构造函数和=操作符也设为私有,防止被复制
  • 使用类的私有静态指针变量指向类的唯一实例
  • 使用一个公有静态方法获取该实例
初始版本

根据这个思路的实现如下:

// 不完善的类
class Singleton
{
private:
		Singleton() {};
		~Singleton() {};
		Singleton(const Singleton&);
		Singleton& operator=(const Singleton&);
		
private:
		static Singleton* instance;
		
public:
		static Singleton* getInstance()
		{
    		if (instance == NULL)
        		instance = new Singleton();
 
   		  return instance;
		}
};

// 初始化静态成员
Singleton* Singleton::instance = NULL;

仔细观察,会发现这个例程只创建了对象,而没有释放,存在内存泄漏的问题。

有两种解决办法:

  • 使用智能指针
  • 使用静态的嵌套类对象
单线程可运行版本

嵌套类对象的实现如下:

// 不完善的类
class Singleton
{
private:
		Singleton() {};
		~Singleton() {};
		Singleton(const Singleton&);
		Singleton& operator=(const Singleton&);
		
private:
		static Singleton* instance;

private:
		class Deletor
		{
		public:
				~Deletor()
				{
						if (Singleton::instance != NULL)
								delete Singleton::instance;
				}
		};
		static Deletor deletor;
		
public:
		static Singleton* getInstance()
		{
    		if (instance == NULL)
        		instance = new Singleton();
 
   		  return instance;
		}
};

// 初始化静态成员
Singleton* Singleton::instance = NULL;

在程序运行结束时,系统会调用静态成员deletor的析构函数,该析构函数会删除单例的唯一实例,方法有如下特征:

  • 在单例类内部定义专有的嵌套类
  • 在单例类内部定义私有的专门用于释放的静态成员
  • 利用程序在退出时析构全局变量的特性释放对象

这个程序在单线程环境下是能够运行的,但是对于多线程,则可能会创建多个实例,如多个线程同时进入if (instance == NULL)语句。

线程安全版本

最简单的办法是使用锁保证线程安全,代码如下:

static Singleton* getInstance()
{
		if (instance == NULL)
		{
				Lock lock;
				if (instance == NULL)
    				instance = new Singleton();
		}
	  return instance;
}

如上的双检测锁模式(DCL: Double-Checked Locking Pattern),在保证线程安全的情况下又尽量减少对运行效率的影响。

但是,该实现仍然存在问题,设想一个线程运行到了第7行,另一个线程运行到第3行,发现instance已经就绪并返回实例时,然而该实例还未完成构建时,会发生错误。

换句话说,就是代码中第2行:if(instance == NULL)和第六行instance = new Singleton();没有正确的同步。

在C++11没有出来的时候,只能靠插入两个memory barrier(内存屏障)来解决这个错误,但是C++11引进了memory model,提供了Atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。

优雅版本

另外,C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。

这样,只有当第一次访问getInstance()方法时才创建实例。这种方法也被称为Meyers’ Singleton。C++0x之后该实现是线程安全的,C++0x之前仍需加锁。

// 完善的类
class Singleton
{
private:
		Singleton() {};
		~Singleton() {};
		Singleton(const Singleton&);
		Singleton& operator=(const Singleton&);
		
public:
		static Singleton& getInstance()
		{
    		static Singleton instance;
 
   		  return instance;
		}
};

以上实现在单例实例第一次被使用时才进行初始化,即延迟初始化,称为懒汉模式(Lazy Singleton)。

饿汉版

饿汉版(Eager Singleton):指单例实例在程序运行时被立即执行初始化。

// 饿汉版本
class Singleton
{
private:
		Singleton() {};
		~Singleton() {};
		Singleton(const Singleton&);
		Singleton& operator=(const Singleton&);
		
private:
		static Singleton instance;
		
public:
		static Singleton& getInstance()
		{
   		  return instance;
		}
};

// 默认直接初始化
Singleton Singleton::instance;

由于在main函数之前初始化,所以没有线程安全的问题。

但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。也即,static Singleton instance;和static Singleton& getInstance()二者的初始化顺序不确定,如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。

总结


  • 饿汉模式线程安全,但存在潜在问题
  • 懒汉模式中局部静态变量版本在C++11后是线程安全的,使用之前版本需要加锁

正因为单例模式使用广泛,所以在面试中也是最常被问到的问题。

在实际学习中,应结合实际应用使用,学以致用,能够更加深刻地理解单例模式。

参考资料


C++ 单例模式

你可能感兴趣的:(设计模式)