文章目錄
1. 題目來源
《劍指-Offer》第二版,P32,面試題2:實現Singleton模式
2. 題目說明
設計一個類,我們只能生成該類的一個實例
3. 題目解析
3.1 單例模式爲什麼常考?
只能生成一個實例的類是實現了Singleton
(單例)模式的類型。設計模式在面向對象程序設計中起着舉足輕重的作用,在面試過程中很多公司都喜歡問一些與設計模式相關的問題。在常用的模式中,Singleton
是唯一 一個能夠用短短几十行代碼完整實現的模式。因此,寫一個 Singleton
的類型是一個很常見的面試題。
在博主的[C++系列]
中也對單例模式進行了講解、實現,可參考以下兩篇博文:
[C++系列] 42. 餓漢模式剖析—單例模式
[C++系列] 43. 懶漢模式剖析—單例模式
3.2 不好的解法一:只使用與單線程環境
由於要求只能生成一個實例,因此我們必須把構造函數設爲私有函數以禁止他人創建實例。我們可以定義一個靜態的實例,靜態成員在程序運行之前完成初始化,並提供一個靜態方法獲取單例靜態成員。下面定義類型Singleton1
就是基於這個思路的實現,四大實現要點如下:
- 構造函數私有
- 定義一個單例靜態成員,靜態成員在程序運行之前完成初始化
- 提供一個靜態方法獲取單例靜態成員
- 防拷貝
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
中,只有在_sin
爲nullptr
的時候才創建一個實例以避免重複創建。同時我們封死構造函數、拷貝構造、賦值運算符,這樣就能確保只創建一個實例。.
3.3 不好的解法二:雖然在多線程環境中能工作但效率不高
解法一中的懶漢模式代碼在單線程的時候工作正常,但在多線程的情況下就有問題了。設想如果兩個線程同時運行到判斷getInstance
是否爲 nullptr
的 if
語句,並且_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 中只有當 _sin
爲 nullprtr
即沒有創建時,需要加鎖操作。當 _sin
已經創建出來之後,則無須加鎖。因爲只在第一次的時候 _sin
爲 nullptr
,因此只在第一次試圖創建實例的時候需要加鎖。這樣 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++
中難道它智能指針
不香嗎~~~
在此挖個坑,待填~~