單例模式

什麼是單例模式

單例模式是一種常用的軟件設計模式。在它的核心結構中只包含一個被稱爲單例的特殊類。通過單例模式可以保證系統中,應用該模式的一個類只有一個實例。即一個類只有一個對象實例。

單例模式的實現

實現單例模式一般有兩種方式:餓漢模式和懶漢模式。

餓漢模式

我們用一個洗碗的例子來解釋餓漢模式和懶漢模式。
現在有餓漢和一個髒了的碗,餓漢希望下次想吃飯的時候能直接盛飯吃,所以他就先把碗先洗好,這樣隨時都有乾淨的碗可以用。

同樣,我們在程序運行之初就將對象創建好,就是說不管你將來用不用,程序啓動時就創建一個唯一的實例對象。這樣程序運行中想要調用這個實例時都可以直接使用。


結合上面的思想,我們想在程序運行之初就要設置好這個實例,很自然的,我們就想到了靜態變量。再將構造函數/拷貝構造函數/賦值運算符重載統統禁掉(設置成私有,類外就無法調用了),就能保證實例不會通過別的方式創建。

然後我們再定義一個函數,這個函數返回單例的地址,就能保證我們能操作同一個單例了。(注意,這個函數必須設置成靜態的,倘若不設置成靜態的,那麼我們只有創建了對象才能調他,但是我們的初衷又是不要通過別的方式創建對象,這就成了先有雞還是先有蛋的問題了)。

class Hungry
{
public:
	static Hungry* GetInstance()//對外提供一個靜態的成員函數,返回我們的單例
	{
		return &onlyInstance;
	}

private:
	static Hungry onlyInstance;//定義一個靜態的實例

private://將其他可能產生實例的函數都弄成私有,設置爲刪除函數是C++11的新寫法
	Hungry() {};
	Hungry(const Hungry&) = delete;
	Hungry& operator=(const Hungry&) = delete;
	
};

Hungry Hungry::onlyInstance;//單例別忘了類外初始化

int main()
{
	Hungry* a = Hungry::GetInstance();
}

餓漢模式的優點:

  1. 簡單
  2. 因爲我們在程序啓動時就設置好了單例,所以餓漢模式是線程安全的。

餓漢模式的缺點:

  1. 會拖慢程序啓動的速度。
  2. 如果有多個單例類對象實例啓動順序不確定。

懶漢模式

懶漢和餓漢不一樣,他看了一眼髒的碗,內心沒有絲毫波動,根本不想洗碗。等他餓了的時候,才慢吞吞的去把碗洗了,才盛飯。

那對應到程序中,只有我們首次想要使用單例的時候,纔去創建單例,這就是懶漢模式。

如果單例對象構造十分耗時或者佔用很多資源,比如加載插件啊, 初始化網絡連接啊,讀取文件啊等
等,而有可能該對象程序運行時不會用到,那麼也要在程序一開始就進行初始化,就會導致程序啓動時非常的緩慢。 所以這種情況使用懶漢模式(延遲加載)更好.


#include<iostream>
#include<mutex>
#include<thread>
using namespace std;
class Lazy
{
public:

	class gc//內嵌垃圾回收類
	{
	public:
		~gc()
		{
			if (Lazy::OnlyInstance)
				delete Lazy::OnlyInstance;
		}
	};

	static gc g;//定義一個靜態成員變量,程序結束時,系統會自動調用它的析構函數從而釋放單例對象

	static Lazy* GetInstance()//返回單例
	{
		if (OnlyInstance == nullptr)
		{
			m_mutex.lock();
			if (OnlyInstance == nullptr)//?爲什麼要雙層檢查
			{
				OnlyInstance = new Lazy();
			}
			m_mutex.unlock();
		}
		return OnlyInstance;
	}

private:
	static Lazy* OnlyInstance;//單例指針
	static mutex m_mutex;//互斥量
private:
	Lazy() {};
	Lazy(const Lazy&) {};
	Lazy& operator=(const Lazy&) {};
};

Lazy* Lazy::OnlyInstance = nullptr;//初始化
mutex Lazy::m_mutex;
Lazy::gc g;



在懶漢模式中,我們用一個靜態的指針變量來指向我們的單例,靜態指針的初始化讓我們在程序啓動上沒有太大的壓力。定義一個GetInstance接口,返回單例指針。

要注意,因爲懶漢模式的單例是有需要纔去創建的,所以在首次創建實例要考慮到線程安全的問題。

	static Lazy* GetInstance()//返回單例
	{
		if (OnlyInstance == nullptr)         //1                  
		{
			m_mutex.lock();
			if (OnlyInstance == nullptr)     //2
			{
				OnlyInstance = new Lazy();
			}
			m_mutex.unlock();
		}
		return OnlyInstance;
	}

當兩個線程並行執行的時候,可能他們都判斷實例未被創建,所以實例就會創建多個。就不滿足單例模式的特性了。

要想讓懶漢模式變得線程安全,那隻要將對公共資源的操作原子化就可以了。而我們熟悉的方式就是加鎖。加鎖的步驟很簡單,這裏只說雙重檢查的原因

我們看上面的代碼,發現在1,2兩處都加了檢查機制,爲什麼要兩層檢查呢?

首先說第一層檢查:

當一個線程運行到第一層檢查,如果發現實例已經創建,就直接返回。
如果沒有創建則通過第一層檢查。
進入第一層檢查後,我們讓各個線程競爭鎖,競爭到鎖的線程(我們用A表示)來進行第二層檢查

第二層檢查:
當線程A獲取鎖後,再次判斷,如果實例仍未被創建,則創建實例。如果實例已經被創建,那麼就是上次拿到鎖的線程(B)創建了實例(B創建的時候A一直阻塞在獲得鎖的地方,A並不知道B先於他獲得了鎖,如果不再次判斷,那麼仍然有可能創建多個實例)。返回即可。

懶漢模式的優點:

  1. 第一次要使用單例的時候纔去創建, 進程啓動的時候無負載。
  2. 多個單例啓動順序可以自由控制多個單例啓動順序可以自由控制

缺點:

  1. 代碼複雜
  2. 需要考慮線程安全,由用戶加鎖
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章