[劍指-Offer] 2. 實現Singleton模式(單例模式、細節處理、代碼優化)

1. 題目來源

《劍指-Offer》第二版,P32,面試題2:實現Singleton模式

2. 題目說明

設計一個類,我們只能生成該類的一個實例

3. 題目解析

3.1 單例模式爲什麼常考?

只能生成一個實例的類是實現了Singleton (單例)模式的類型。設計模式在面向對象程序設計中起着舉足輕重的作用,在面試過程中很多公司都喜歡問一些與設計模式相關的問題。在常用的模式中,Singleton 是唯一 一個能夠用短短几十行代碼完整實現的模式。因此,寫一個 Singleton 的類型是一個很常見的面試題。

在博主的[C++系列] 中也對單例模式進行了講解、實現,可參考以下兩篇博文:

[C++系列] 42. 餓漢模式剖析—單例模式
[C++系列] 43. 懶漢模式剖析—單例模式

3.2 不好的解法一:只使用與單線程環境

由於要求只能生成一個實例,因此我們必須把構造函數設爲私有函數以禁止他人創建實例。我們可以定義一個靜態的實例,靜態成員在程序運行之前完成初始化,並提供一個靜態方法獲取單例靜態成員。下面定義類型Singleton1 就是基於這個思路的實現,四大實現要點如下

  1. 構造函數私有
  2. 定義一個單例靜態成員,靜態成員在程序運行之前完成初始化
  3. 提供一個靜態方法獲取單例靜態成員
  4. 防拷貝
class singleton1 {
public:
	static singleton1* getinstance() {
		return &m_instance;
	}
private:
	// 1. 構造函數私有 
	singleton1() {};
 
	// 2. 採用 c++11刪除函數  拷貝函數、賦值運算符私有
	singleton1(singleton1 const&) = delete;
	singleton1& operator=(singleton1 const&) = delete;
	
	static singleton1 m_instance;    
};
 
singleton1 singleton1::m_instance; 

下面爲單線程懶漢模式:

// 單線程懶漢模式
class Singleton
{
public:
	static Singleton* getInstance() {
		// 提高後續線程調用接口的效率
		if (_sin == nullptr) {       // 第一次爲空,創建對象,第二次非空,直接返回,保證單例
			_sin = new Singleton;
		}
		return _sin;
	}
private:
	Singleton(){}       
	Singleton(const Singleton& s) = delete;

	static Singleton* _sin;        // 定義爲指針,與對象不爲同一類型,其爲單獨的指針類型
};

Singleton* Singleton::_sin = nullptr;

上述單線程懶漢模式代碼在Singleton的靜態函數getInstance 中,只有在_sinnullptr的時候才創建一個實例以避免重複創建。同時我們封死構造函數、拷貝構造、賦值運算符,這樣就能確保只創建一個實例。.

3.3 不好的解法二:雖然在多線程環境中能工作但效率不高

解法一中的懶漢模式代碼在單線程的時候工作正常,但在多線程的情況下就有問題了。設想如果兩個線程同時運行到判斷getInstance是否爲 nullptrif 語句,並且_sin 的確沒有創建時,那麼兩個線程都會創建一個實例,此時類型 Singleton 就不再滿足單例模式的要求了。爲了保證在多線程環境下我們還是隻能得到類型的一個實例,需要加上一個同步鎖。把 Singleton 稍做修改得到了如下代碼:

// 單線程懶漢模式
#include <mutex>  // 加鎖頭文件,互斥鎖,所有的線程共用同一把鎖,全局只有一把鎖,用一把鎖限制所有線程

class Singleton {
private:
    static mutex _mtx;	              // 全局只有一把鎖,限制全部線程
public:
	static Singleton* getInstance() {
	    _mtx.lock();                  // 加鎖不能在if內,沒有意義,還是要創建對象
	    if (_sin == nullptr) {        // 第一次爲空,創建對象,第二次非空,直接返回,保證單例
	        _sin = new Singleton;
	    }
	    _mtx.unlock();
	}
private:
	// 1. 構造函數私有化   2. 拷貝構造私有化(不必實現)   3. 賦值運算符無所謂私有化,因爲其不創建新的對象
	Singleton(){}       
	Singleton(const Singleton& s) = delete;

	static Singleton* _sin;        // 定義爲指針,與對象不爲同一類型,其爲單獨的指針類型
};

Singleton* Singleton::_sin = nullptr;

3.4 可行的解法:加同步鎖前後兩次判斷實例是否已存在

我們只是在實例還沒有創建之前需要加鎖操作,以保證只有一 一個線程創建出實例。而當實例已經創建之後,我們已經不需要再做加鎖操作了。於是我們可以把解法二中的代碼再做進一步的改進:

// 單線程懶漢模式
#include <mutex>  // 加鎖頭文件,互斥鎖,所有的線程共用同一把鎖,全局只有一把鎖,用一把鎖限制所有線程

class Singleton {
private:
    static mutex _mtx;	              // 全局只有一把鎖,限制全部線程
public:
	static Singleton* getInstance() {
	    if (_sin == nullptr) {
	        _mtx.lock();                 // 加鎖不能在if內,沒有意義,還是要創建對象
	        if (_sin == nullptr) {       // 第一次爲空,創建對象,第二次非空,直接返回,保證單例
				_sin = new Singleton;
	        }
	    	_mtx.unlock();
	    }
	    return _sin;
	}
private:
	// 1. 構造函數私有化   2. 拷貝構造私有化(不必實現)   3. 賦值運算符無所謂私有化,因爲其不創建新的對象
	Singleton(){}       
	Singleton(const Singleton& s) = delete;

	static Singleton* _sin;        // 定義爲指針,與對象不爲同一類型,其爲單獨的指針類型
};

Singleton* Singleton::_sin = nullptr;

3.4 中只有當 _sinnullprtr 即沒有創建時,需要加鎖操作。當 _sin 已經創建出來之後,則無須加鎖。因爲只在第一次的時候 _sinnullptr,因此只在第一次試圖創建實例的時候需要加鎖。這樣 3.4 的時間效率比 3.3 要好很多。

3.4 中用加鎖機制來確保在多線程環境下只創建一個實例,並且用兩個 if 判斷來提高效率,實現Double-check,這個是很重要的點 。這樣的代碼實現起來比較複雜,容易出錯,我們還有更加優秀的解法。

3.5 強烈推薦的解法一:利用靜態構造函數

C# 有靜態構造函數的寫法,在此我也沒學習過 C#,故不作討論,可參見書本的 P34-強烈推薦的解法一:利用靜態構造函數中所講。在此主要關注 強烈推薦的解法二:實現按需創建實例。

3.6 強烈推薦的解法二:內部類寫法

原書中的寫法時基於 3.5C#中利用靜態構造函數進行的內部類寫法,在此沒辦法對其進行拓展。但在 C++ 中恰好可以通過內部類寫法來手動釋放內存,更加的巧妙和精細的進行了安全的內存管理,這是每一個 C++ 程序員所希望的!下面來看看實現的思路及代碼:

在調用 getInstance 時,用 new 申請了空間,但用完我們並沒有釋放空間。現在,也不需要手動去釋放單例不僅僅在一個地方使用,可能也在其它地方使用,釋放了會導致程序崩潰。

Singleton* ps = Singleton::GetInstance();
delete ps;
ps = nullptr;

所以我們只能 delete ps,再將 ps 置空,但是,將 ps 空間釋放之後,類中的空間又沒有被釋放,還是一個有效值。 而且不光在此使用這個空間,在其他的地方到該空間的接口,一開始調用的時候該指針有效,但在此已經被釋放了,會出現解引用的錯誤。不能手動去釋放。

在此,一般可以不用管,因爲其爲靜態成員,在整個程序運行週期內均有效,程序運行結束即進程結束,那麼會將所有的空間資源均返還給系統,達到垃圾回收的目的。

但是若是想手動釋放的話,可以在內部定義一個內部類輔助操作。內部類可以訪問外部類的私有成員,並且可以直接訪問。

class Singleton {
public:
	static Singleton* getInstance() {
		if (_sin == nullptr) {
			_mtx.lock();                 
			if (_sin == nullptr) {       // 第一次爲空,創建對象,第二次非空,直接返回,保證單例
				_sin = new Singleton;
			}
			_mtx.unlock();
		}
		return _sin;
	}
	
	class GC {     // 定義內部類,進行垃圾回收
	public:
		~GC() {
			if (_sin) {
				delete _sin;
				_sin = nullptr;
			}
		}
	};

private:
	
	Singleton(){}       
	Singleton(const Singleton& s) = delete;

	static Singleton* _sin;       
	static mutex _mtx;			 
	static GC _gc;
};

Singleton* Singleton::_sin = nullptr;
mutex Singleton::_mtx;
Singleton::GC Singleton::_gc; // 它是靜態成員,其生命週期也是整個程序的生命週期,調用析構函數釋放空間

爲什麼要採用內部類來做這樣一個事情呢?爲什麼不能在單例上直接寫析構函數進行資源回收呢?

class Singleton {
public:
	static Singleton* getInstance() {
		if (_sin == nullptr) {
			_mtx.lock();                
			if (_sin == nullptr) {      
				_sin = new Singleton;
			}
			_mtx.unlock();
		}
		return _sin;
	}
	~Singleton() {    // 單例中析構函數產生遞歸效果
		if (_sin) {   // 之前所產生的對象不爲當前類,不會重複遞歸調用析構函數
			delete _sin;
			_sin = nullptr;
		}
	}
	

~Singleton,會在 delete _sin 上重複調用析構函數產生遞歸效應。因爲之前調用析構函數釋放的資源不是當前類類型的,不會去遞歸調用當前類的析構函數,而再次調用剛好觸發了該條件。 寫這麼多,在C++難道它智能指針不香嗎~~~

在此挖個坑,待填~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章