天天看點

設計模式——單例模式

設計模式:

設計模式代表了最佳實踐,是軟體開發過程中面臨一般問題的解決方案。 設計模式是一套被反複使用、經過分類、代碼設計總結的經驗。

單例模式

單例模式也叫單件模式。Singleton是一個非常常用的設計模式,幾乎所有稍微大一些的程式都會使用到它,是以建構一個線程安全并且 高效的Singleton很重要。

1. 單例類保證全局隻有一個唯一執行個體對象。

2. 單例類提供擷取這個唯一執行個體的接口。

由于要求隻生成一個執行個體,是以我們必須把構造函數的通路權限标記為protected或private,限制隻能在類内建立對象.

單例類要提供一個通路唯一執行個體的接口函數(全局通路點),就需要在類中定義一個static函數,傳回在類内部唯一構造的執行個體。

(這樣還可以確定直接用類名就能通路到該唯一執行個體,不必用到執行個體化出的對象名去調用)

 兩個概念:

 懶漢模式 (lazy loading ):第一次調用GetInstance才建立執行個體對象,比較複雜

 餓漢模式:  程式一運作,就建立執行個體對象、簡潔高效 ,但有些場景下不适用 

方法一:不考慮線程安全,隻适用于單線程環境的單例類

定義一個靜态的執行個體,在需要的時候建立該執行個體 (懶漢模式)

class Singleton
{
public:
	//擷取唯一對象執行個體的接口函數
	static Singleton* GetInstance()
	{
		if (_instance == NULL)
		{
			_instance = new Singleton();
		}
		return _instance;
	}
	static void DelInstance()
	{
		if (_instance != NULL)
		{
			delete _instance;
			_instance = NULL;
		}
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//構造函數标記為protected或private,限制隻能在類内建立對象
	Singleton()
		:_data(5)
	{}

	//防拷貝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);
private:		
	//指向執行個體的指針定義為靜态私有,這樣定義靜态成員函數擷取對象執行個體
	static Singleton* _instance;	  // 單執行個體對象
	int _data;  //單執行個體對象中的資料
};
// 靜态成員在類外初始化
Singleton* Singleton::_instance = NULL;
      

  這種方法是最簡單、最普遍的方法。隻有在_instance為NULL的時候才會建立一個執行個體以避免重複建立。同時我們把構造函數定義為私有函數,這樣就能確定隻建立一個執行個體。

但是上述的代碼在單線程的時候工作正常,在多線程的情況下就有問題了。

  設想如果兩個線程同時運作到判斷_instance是否為NULL的 if 語句那裡,并且_instance之前并未建立時,這兩個線程各自就都會建立一執行個體,這是就無法滿足單例模式的要求了。

 方法二:能在多線程環境下工作,但是效率不高

為了保障在多線程環境下隻得到一個執行個體,需要加一把互斥鎖。把上述代碼稍作修改,即:

ps: 下面部分的加鎖使用了C++11庫的互斥鎖

class Singleton
{
public:
	//擷取唯一對象執行個體的接口函數
	static Singleton* GetInstance()
	{
		//lock();        //C++中沒有直接的lock()
		//RAII
		//lock lk;
		_sMtx.lock();   //C++11
		if (_instance == NULL)
		{
			_instance = new Singleton();
		}
		//unlock();
		_sMtx.unlock();
		return _instance;
	}
	static void DelInstance()
	{
		if (_instance != NULL)
		{
			delete _instance;
			_instance = NULL;
		}
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//構造函數标記為protected或private,限制隻能在類内建立對象
	Singleton()
		:_data(5)
	{}

	//防拷貝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);

private:
	//指向執行個體的指針定義為靜态私有,這樣定義靜态成員函數擷取對象執行個體
	static Singleton* _instance;	  // 單執行個體對象
	int _data;								  // 單執行個體對象中的資料
	static mutex _sMtx;	              // 互斥鎖
};
// 靜态成員在類外初始化
Singleton* Singleton::_instance = NULL;
mutex Singleton::_sMtx;
      

  設想有兩個線程同時想建立一個執行個體,由于在一個時刻,隻有一個線程能得到互斥鎖,是以當第一個線程加上鎖後,第二個線程就隻能等待。當第一個線程發現執行個體還沒有建立時,它就建立一個執行個體。接着第一個線程釋放鎖,此時第二個線程進入并上鎖,這個時候由于執行個體已經被第一個線程建立出來了,第二個線程就不會重複建立執行個體了,這樣就保證在多線程環境下隻能得到一個執行個體。

  但是,每次擷取唯一執行個體,程式都會加鎖,而加鎖是一個非常耗時的操作,在沒有必要的時候,我們要盡量避免,否則會影響性能。

 方法三:使用雙重檢查,提高效率,避免高并發場景下每次擷取執行個體對象都進行加鎖,并使用記憶體栅欄防止重排

class Singleton
{
public:
	//擷取唯一對象執行個體的接口函數
	static Singleton* GetInstance()
	{
		// 使用雙重檢查,提高效率,避免高并發場景下每次擷取執行個體對象都進行加鎖
		if (_instance == NULL)
		{
			std::lock_guard<std::mutex> lck(_sMtx);
			if (_instance == NULL)
			{
				// tmp = new Singleton()分為以下三個部分
				// 1.配置設定空間2.調用構造函數3.指派
				// 編譯器編譯優化可能會把2和3進行指令重排,這樣可能會導緻高并發場景下,其他線程擷取到未調用構造函數初始化的對象
				// 以下加入記憶體栅欄進行處理,防止編譯器重排栅欄後面的指派到記憶體栅欄之前
				Singleton* tmp = new Singleton();
				MemoryBarrier(); //記憶體栅欄
				_instance = tmp;
			}
		}
		return _instance;
	}
	static void DelInstance()
	{
		if (_instance != NULL)
		{
			delete _instance;
			_instance = NULL;
		}
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//構造函數标記為protected或private,限制隻能在類内建立對象
	Singleton()
		:_data(5)
	{}

	//防拷貝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);

private:
	//指向執行個體的指針定義為靜态私有,這樣定義靜态成員函數擷取對象執行個體
	static Singleton* _instance;	  // 單執行個體對象
	int _data;								  // 單執行個體對象中的資料
	static mutex _sMtx;	              // 互斥鎖
};
// 靜态成員在類外初始化
Singleton* Singleton::_instance = NULL;
mutex Singleton::_sMtx;
      

  試想,當執行個體還未建立時,由于 Singleton == NULL ,是以很明顯,兩個線程都可以通過第一重的 if 判斷 ,進入第一重 if 語句後,由于存在鎖機制,是以會有一個線程進入 lock 語句并進入第二重 if 判斷 ,而另外的一個線程則會在 lock 語句的外面等待。而當第一個線程執行完 new  Singleton()語句退出鎖定區域,第二個線程便可以進入 lock 語句塊,此時,如果沒有第二重Singleton == NULL的話,那麼第二個線程還是可以調用 new  Singleton()語句,第二個線程仍舊會建立一個 Singleton 執行個體,這樣也還是違背了單例模式的初衷的,是以這裡必須要使用雙重檢查鎖定(第二層if 判斷必須存在)。

   多數現代計算機為了提高性能而采取亂序執行,這使得記憶體栅欄成為必須。barrier就象是代碼中的一個栅欄,将代碼邏輯分成兩段,barrier之前的代碼和barrier之後的代碼在經過編譯器編譯後順序不能亂掉。也就是說,barrier之後的代碼對應的彙編,不能跑到barrier之前去,反之亦然。之是以這麼做是因為在我們這個場景中,如果編譯器為了榨取CPU的performace而對彙編指令進行重排,其它線程擷取到未調用構造函數初始化的對象,很有可能導緻出錯。

   隻有第一次調用_instance為NULL,并且試圖建立執行個體的時候才需要加鎖,當_instance已經建立出來後,則沒必要加鎖。這樣的修改比之前的時間效率要好很多。

但是這樣的實作比較複雜,容易出錯,我們還可以利用餓漢模式,建立相對簡潔高效的單例模式。

方法四:餓漢模式--簡潔、高效、不用加鎖、但是在某些場景下會有缺陷

  因為靜态成員的初始化在程式開始時,也就是進入主函數之前,由主線程以單線程方式完成了初始化,是以靜态初始化執行個體保證了線程安全性。在性能要求比較高時,就可以使用這種方式,進而避免頻繁的加鎖和解鎖造成的資源浪費。

class Singleton
{
public:
	//擷取唯一對象執行個體的接口函數
	static Singleton* GetInstance()
	{
		assert(_instance);
		return _instance;
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//構造函數标記為protected或private,限制隻能在類内建立對象
	Singleton()
		:_data(5)
	{}

	//防拷貝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);

private:
	static Singleton* _instance;	  // 單執行個體對象
	int _data;			 // 單執行個體對象中的資料
};
Singleton* Singleton::_instance = new Singleton;
      

 代碼實作非常簡潔。建立的執行個體_instance并不是在第一次調用GetInstance接口函數時才建立,而是在初始化靜态變量的時候就建立一個執行個體。如果按照該方法會過早的建立執行個體,進而降低記憶體的使用效率。 

方法五:方法四還可以再簡化點

class Singleton
{
public:
	//擷取唯一對象執行個體的接口函數
	static Singleton* GetInstance()
	{
		static Singleton instance;
		return &instance;
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//構造函數标記為protected或private,限制隻能在類内建立對象
	Singleton()
		:_data(5)
	{}

	//防拷貝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);

private:
	int _data;	 // 單執行個體對象中的資料
};
      

 執行個體銷毀

 此處使用了一個内部GC類,而該類的作用就是用來釋放資源

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//帶RAII GC自動回收執行個體對象的方式
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

class Singleton
{
public:
	// 擷取唯一對象執行個體的接口函數
	static Singleton* GetInstance()
	{
		assert(_instance);
		return _instance;
	}
	// 删除執行個體對象
	static void DelInstance()
	{
		if (_instance)
		{
			delete _instance;
			_instance = NULL;
		}
	}
	void Print()
	{
		cout << _data << endl;
	}
	class GC
	{
	public:
		~GC()
		{
			cout << "DelInstance()" << endl;
			DelInstance();
		}
	};
private:
	Singleton()
		:_data(5)
	{}
	static Singleton*_instance;
	int _data;
};
// 靜态對象在main函數之前初始化,這時隻有主線程運作,是以是線程安全的。
Singleton* Singleton::_instance = new Singleton;
// 使用RAII,定義全局的GC對象釋放對象執行個體
Singleton::GC gc;
      

    在程式運作結束時,系統會調用Singleton中GC的析構函數,該析構函數會進行資源的釋放。

繼續閱讀