C++ 實現單例模式小結

單例模式

1.1定義以及作用

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。保證一個類只有一個對象,降低對象之間的耦合度

1.2 特點

某個類只能有一個實例;其必須自行創建這個實例;必須自行向整個系統提供這個實例。
優點:

  • 活動的單例只有一個實例,對單例類的所有實例化得到的都是相同的一個實例。這樣就防止了其他對象對自己就行實例化,確保每個對象都訪問一個實例
  • 提供了對唯一實例的受控訪問
  • 避免對共享資源的多重佔用
  • 允許可變數目的實例
  • 可節約系統資源,無須頻繁的創建和銷燬對象

缺點:

  • 不適用變化的對象,若同一類型的對象總是要在不同的用例場景發生變化,則會一起數據的錯誤
  • 由於沒有抽象層,因此可擴展難度大
  • 其職責過重,在一定程度上違背了“單一職責原則”

1.3 UML類圖

單例模式

1.4 單例模式說明

單例模式下又分爲懶漢模式和餓漢模式。
懶漢模式:顧名思義——懶,即只有在第一次用到類實例的時候纔會去實例化。(懶漢模式本身是線程不安全的)。特點 :在訪問量小時,採用懶漢模式,以時間換空間。
餓漢模式:單例類加載的時候就將自己以靜態初始化的方式實例化。特點:由於要進行線程同步,因此在訪問量比較大或可能訪問的線程比較多時,採用餓漢模式。以空間換時間。

適用場景:

  • 需要頻繁實例化然後銷燬的對象
  • 創建對象耗時過多或者耗資源過多,但又經常用到的對象
  • 有狀態的工具類對象
  • 頻繁訪問數據庫或文件的對象
  • 其他只有一個對象的場景

1.5 代碼實現

測試平臺VS2015,單例模式的有以下幾種解法:

1.5.1 示例代碼一:單線程解法

缺點:多線程情況下,每個線程可能創建出不同的Singleton實例

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <windows.h>
using namespace std;

#define MAX_Count 10
#define init_mode 1    //測試兩種不同的new實例化方式 
//1--表示在類中new Singleton()  即懶漢模式
//0--表示在類外new Singleton()

class Singleton
{
public:
	static Singleton* getInstance() {	
#if init_mode == 1
		if (m_pInstance == NULL)
		{
			Sleep(100);    //放大多線程操作的效果
			m_pInstance = new Singleton();
		}
#else
		Sleep(100);    //放大多線程操作的效果 爲了與類中實例化等效 控制變量 
#endif // init_mode == 1

		return m_pInstance;
	}

	static void destoryInstance() {
		if (m_pInstance != NULL)
		{
			delete m_pInstance;
			m_pInstance = NULL;
		}
	}
private:
	Singleton() {}
	static Singleton* m_pInstance;
};

//Singleton實例初始化
#if init_mode == 1
//這裏線程是不安全的
    Singleton* Singleton::m_pInstance = nullptr;
#else
//前面不能加static,會和類外全局static混淆
//在這裏實例化時 線程安全的 static初始化是在主函數main()之前
    Singleton* Singleton::m_pInstance = new Singleton();
#endif // init_mode == 1

void print_singleton_instance() {
	Singleton *sObj = Singleton::getInstance();
	cout << sObj << endl;
}

//多線程獲取多次實例
void multithreading_test() {
// 預期結果:所有的實例指針指向的地址相同
		vector<thread> threads;
		for (int i = 0; i < MAX_Count; ++i) {
			threads.push_back(thread(print_singleton_instance));
		}
	
		for (auto& t : threads) {
			t.join();
		}
#if init_mode == 1
		Singleton::destoryInstance();
#endif // init_mode == 1
}

//單線程獲取多次實例
void singlethreading_test() {
// 預期結果:所有的實例指針指向的地址相同
	vector<Singleton*> s;

	for (int i = 0; i < MAX_Count; ++i) {
	    s.push_back(Singleton::getInstance());
		cout << s[i] << endl;
	}
    
//爲什麼0的時候不銷燬 由於我在main裏面多線程和單線程的一起測試
//若在0的銷燬,後面測試多線程的時候,地址爲空    
#if init_mode == 1
	Singleton::destoryInstance();
#endif // init_mode == 1
}
//test
int main() {

	cout << "singlethreading_test : " << endl;
	singlethreading_test();
	cout << "multithreading_test : " << endl;
	multithreading_test();

	getchar();
	return 0;
}

測試對比如下:

測試類型 類外初始化(init_mode=0 類中初始化(init_mode=1
代碼測試結果 類外實例化 在這裏插入圖片描述
結果分析 單線程中的地址沒變,多線程地址也沒變 多線程測試下地址不一樣

小結:單線程解法在不同的實例化方法情況下,在多線程中每個線程可能創建出不同的Singleton實例

1.5.2 示例代碼二:多線程+加鎖

由於在示例一中,存在多線程情況下,可能創建多個實例的問題,因此改善其代碼,構造的時候加鎖。測試平臺依然是VS2015,修改部分的代碼如下:

class Singleton
{
public:
	static Singleton* getInstance() {
#if init_mode == 1
		//if (m_pInstance == NULL)
		//{
		//	Sleep(100);    //放大多線程操作的效果
		//	m_pInstance = new Singleton();
		//}
		if (m_pInstance == NULL)
		{
			m_mutex.lock(); //使用C++11中的多線程庫
	        if (m_pInstance == NULL)
            {
                Sleep(100);    //放大多線程操作的效果
                m_pInstance = new Singleton();
            }
			m_mutex.unlock();
		}	
#else
		Sleep(100);   //放大多線程操作的效果
#endif // init_mode == 1
		return m_pInstance;
	}

	static void destoryInstance() {
		if (m_pInstance != NULL)
		{
			delete m_pInstance;
			m_pInstance = NULL;
		}
	}

private:
	static mutex m_mutex; //定義互斥量
	Singleton() {}
	static Singleton* m_pInstance;
};

//Singleton實例初始化
//前面不能加static,會和類外全局static混淆
//在這裏實例化時 線程安全的 static初始化是在主函數main()之前
#if init_mode == 1
    Singleton* Singleton::m_pInstance = nullptr;
	mutex Singleton::m_mutex;
#else
    Singleton* Singleton::m_pInstance = new Singleton();
#endif // init_mode == 1

這裏在new Singleton()進行了加鎖操作,同時借鑑大話設計模式中的C#實現的單例模式,使用雙重鎖定(Double-Check Locking)。這樣做的目的是爲了當兩個線程同時調用getInstance()時,都可以通過第一次m_pInstance == NULL的判斷,然後由於Lock機制,這兩個線程則只有一個進入,另一個處於等候狀態。必須等待其中已經進入的線程出來之後,另一個處於等候的線程才能進入。而第二次判斷的這保證了當第一個線程創建了實例,第二個線程進來是不會繼續創建實例,這樣就達到了單例的目的

測試結果如下:

測試類型 雙重鎖定 不加第二次判斷
測試結果 雙重鎖定 在這裏插入圖片描述
結果分析 多線程地址沒變 多線程測試下地址不一樣
1.5.3 示例代碼三:const static型實例

由於頻繁的進行加鎖和解鎖,可能造成資源浪費,因此加鎖操作將成爲一個性能的瓶頸,再次改進代碼,利用const static的特點:const只能定義一次,不能再次修改;static保持類內只有一個實例。代碼如下:

class Singleton
{
public:
	static Singleton* getInstance() {
		Sleep(100);    //放大多線程操作的效果
		return const_cast<Singleton *>(m_pInstance); // 去掉“const”特性;
	}

	static void destoryInstance() {
		if (m_pInstance != NULL)
		{
			delete m_pInstance;
			m_pInstance = NULL;
		}
	}

private:
	Singleton() {}
	static const  Singleton* m_pInstance;
};

//利用const只能定義一次的特點
const Singleton* Singleton::m_pInstance = new Singleton();

void print_singleton_instance() {
	Singleton *sObj = Singleton::getInstance();
	cout << sObj << endl;
}

//多線程獲取多次實例
void multithreading_test() {

		vector<thread> threads;
		for (int i = 0; i < MAX_Count; ++i) {
			threads.push_back(thread(print_singleton_instance));
		}
		for (auto& t : threads) {
			t.join();
		}
		Singleton::destoryInstance();
}
//test
int main() {
	cout << "multithreading_test : " << endl;
	multithreading_test();

	getchar();
	return 0;
}

測試結果如下:
const static
由於靜態初始化在程序開始時,即進入main函數之前,有主線程以單線程的方式完成了初始化,所以靜態初始化實例保證了線程安全。而且性能也得到提高,避免了頻繁的加鎖和解鎖操作。

1.5.4 示例代碼四:在get函數中創建並返回static臨時實例的引用

簡單粗暴的做法,既不加鎖也不使用const,但該方法不能認爲控制單例實例的銷燬。代碼如下:

class Singleton {
public:
	static Singleton* getInstance() {
		Sleep(100);    //放大多線程操作的效果
		static Singleton instance;
		return &instance;
	}

private:
	Singleton() {}
};


void print_singleton_instance() {
	Singleton *sObj = Singleton::getInstance();
	cout << sObj << endl;
}

//多線程獲取多次實例
void multithreading_test() {

	vector<thread> threads;
	for (int i = 0; i < MAX_Count; ++i) {
		threads.push_back(thread(print_singleton_instance));
	}
	for (auto& t : threads) {
		t.join();
	}
}
//test
int main() {
	cout << "multithreading_test : " << endl;
	multithreading_test();

	getchar();
	return 0;
}

測試結果如下:
get函數中實例化

1.5.5 示例代碼五:顯式控制實例銷燬

在上面的代碼示例中,除了示例四沒有使用new實例化對象,其他均使用了new操作符。雖然加入了destoryInstance(),但難免有時候會忘記調用該函數,,以及相關資源的釋放問題。在示例4的缺點是不能人爲控制實例的銷燬。結合上述兩種方法,修改代碼如下:

class Singleton {
public:
	static Singleton* getInstance() {
		Sleep(100);    //放大多線程操作的效果
		return instance;
	}

private:
	Singleton() {}
	static Singleton* instance;

	class GC   //類似C#、Java的垃圾回收
	{
	public:
		~GC() {
			//可以在此處釋放所有需要釋放的資源,比如數據庫、文件等
			if(instance != NULL)
			{
				cout << "GC will run" << endl;
				delete instance;
				instance = NULL;
				cout << "GC completed " << endl;
				getchar();    //增加演示效果
			}
		};
	};

	// 內部類的實例
	static GC gc;
};

Singleton* Singleton::instance = new Singleton();
Singleton::GC Singleton::gc;

void print_singleton_instance() {
	Singleton *sObj = Singleton::getInstance();
	cout << sObj << endl;
}

//單線程獲取多次實例
void singlethreading_test() {
	// 預期結果:所有的實例指針指向的地址相同
	vector<Singleton*> s;
	for (int i = 0; i < MAX_Count; ++i) {
		s.push_back(Singleton::getInstance());
		cout << s[i] << endl;
	}
}

//多線程獲取多次實例
void multithreading_test() {

	vector<thread> threads;
	for (int i = 0; i < MAX_Count; ++i) {
		threads.push_back(thread(print_singleton_instance));
	}
	for (auto& t : threads) {
		t.join();
	}
}
//test
int main() {

	cout << "singlethreading_test : " << endl;
	singlethreading_test();
	cout << "multithreading_test : " << endl;
	multithreading_test();

	getchar();
	return 0;
}

測試結果如下:
GC
這裏GC will run前面的空格是爲了演示效果,main中的getchar()造成。可以看到當程序運行結束時,系統會調用Singleton的靜態成員GC的析構函數,進行資源的釋放,這樣無須人爲控制資源的釋放。其原理是:程序在結束時,系統會自動析構所有的全局變量,也會析構使用類的靜態成員變量,由於靜態變量和全局變量在內存中,均在靜態儲存區,在程序結束後,靜態存儲區的變量都會被釋放

1.5.6 示例代碼六:智能指針實現

程序員總是喜歡精益求精,不斷改進現有的代碼。上述代碼所解決的問題可看作指針創建以及銷燬。而C++11提供了智能指針這個東西,引用計數爲0的時候自動釋放內存,方便內存管理,因此嘗試用智能指針實現。示例代碼如下:

class Singleton {
public:
	static shared_ptr<Singleton> getInstance() {
		Sleep(100);    //放大多線程操作的效果
		return instance;
	}

private:
	Singleton() {}
	Singleton(const Singleton &);
	static shared_ptr<Singleton> instance;
	~Singleton() {
		cout << "單例對象銷燬!" << endl; 
		getchar();
	};

};

shared_ptr<Singleton> Singleton::instance(new Singleton());


void print_singleton_instance() {
	shared_ptr<Singleton> sObj = Singleton::getInstance();
	cout << sObj << endl;
}

//單線程獲取多次實例
void singlethreading_test() {
	// 預期結果:所有的實例指針指向的地址相同
	vector<shared_ptr<Singleton>> s;
	for (int i = 0; i < MAX_Count; ++i) {
		s.push_back(Singleton::getInstance());
		cout << s[i] << endl;
	}
}

//多線程獲取多次實例
void multithreading_test() {

	vector<thread> threads;
	for (int i = 0; i < MAX_Count; ++i) {
		threads.push_back(thread(print_singleton_instance));
	}
	for (auto& t : threads) {
		t.join();
	}
}
//test
int main() {

	cout << "singlethreading_test : " << endl;
	singlethreading_test();
	cout << "multithreading_test : " << endl;
	multithreading_test();

	getchar();
	return 0;
}

出現編譯錯誤:shared_ptr無法訪問private成員。這裏可以將析構函數變爲public,但是這就可能使用戶調用該函數,違背了封閉的原則,另外一種方法就是利用shared_ptr在定義的時候可以指定刪除器(deleter),參考別人的例子,定義一個static類型的銷燬函數,即可通過類名直接調用,示例代碼如下:

class Singleton {
public:
	static shared_ptr<Singleton> getInstance() {
		Sleep(100);    //放大多線程操作的效果
		return instance;
	}

private:
	Singleton() {}
	Singleton(const Singleton &);
	static shared_ptr<Singleton> instance;
	~Singleton() {
		cout << "單例對象銷燬!" << endl; 
		getchar();
	};
    //用來銷燬智能指針對象
    static void Destory(Singleton *) { 
		cout << "在這裏銷燬單例對象!" << endl;
		getchar();
	};
};

shared_ptr<Singleton> Singleton::instance(new Singleton(),Singleton::Destory);

測試結果如下:
shared_ptr
這裏可以看到銷燬智能指針實際上執行的是Destory函數並非~Singleton() ,刪除器聲明時(即static void Destory(Singleton *))需要傳入該對象的指針。原因查看出錯出的代碼得知。出錯處的代碼如下:

private:
	template<class _Ux>
		void _Resetp(_Ux *_Px)
		{	// release, take ownership of _Px
		_TRY_BEGIN	// allocate control block and reset
		_Resetp0(_Px, new _Ref_count<_Ux>(_Px));
		_CATCH_ALL	// allocation failed, delete resource
		delete _Px;
		_RERAISE;
		_CATCH_END
		}

	template<class _Ux,
		class _Dx>
		void _Resetp(_Ux *_Px, _Dx _Dt)
		{	// release, take ownership of _Px, deleter _Dt
		_TRY_BEGIN	// allocate control block and reset
		_Resetp0(_Px, new _Ref_count_del<_Ux, _Dx>(_Px, _Dt));
		_CATCH_ALL	// allocation failed, delete resource
		_Dt(_Px);
		_RERAISE;
		_CATCH_END
		}

當我們利用前面出錯的代碼調用時,即未指定deleter,調用的是第一個_Resetp;而指定指定deleter,調用的是第二個_Resetp,此時deleter _Dt需要傳入一個參數_Px(即shared_ptr內部對象的指針——Singleton *),最後釋放shared_ptr內部對象的內容。

1.6 小結

  總結了單例模式的幾種實現方式,其中還有其他方式還未實現,如使用內存柵欄方式。同時複習和學習了C++11智能指針、線程、const和static等方面的知識。其中C++裏面的相關知識還需要深入學習理解,加深印象。

參考資料

  1. C++完美單例模式–內存柵欄
  2. C++ 單例模式(懶漢、餓漢模式)
  3. C++實現單例模式(包括採用C++11中的智能指針)
  4. C++ 單例設計模式
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章