歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

設計模式——單例模式

設計模式:

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

單例模式

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

1. 單例類保證全局只有一個唯一實例對象。

2. 單例類提供獲取這個唯一實例的接口。

由於要求只生成一個實例,因此我們必須把構造函數的訪問權限標記為protected或private,限制只能在類內創建對象.

單例類要提供一個訪問唯一實例的接口函數(全局訪問點),就需要在類中定義一個static函數,返回在類內部唯一構造的實例。


 兩個概念:

 懶漢模式 (lazy loading ):第一次調用GetInstance才創建實例對象,比較復雜
 餓漢模式:  程序一運行,就創建實例對象、簡潔高效 ,但有些場景下不適用 

方法一:不考慮線程安全,只適用於單線程環境的單例類

定義一個靜態的實例,在需要的時候創建該實例 (懶漢模式)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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庫的互斥鎖

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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;

  設想有兩個線程同時想創建一個實例,由於在一個時刻,只有一個線程能得到互斥鎖,所以當第一個線程加上鎖後,第二個線程就只能等待。當第一個線程發現實例還沒有創建時,它就建立一個實例。接著第一個線程釋放鎖,此時第二個線程進入並上鎖,這個時候由於實例已經被第一個線程創建出來了,第二個線程就不會重復創建實例了,這樣就保證在多線程環境下只能得到一個實例。

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


 方法三:使用雙重檢查,提高效率,避免高並發場景下每次獲取實例對象都進行加鎖,並使用內存柵欄防止重排

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 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已經創建出來後,則沒必要加鎖。這樣的修改比之前的時間效率要好很多。

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


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

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

? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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接口函數時才創建,而是在初始化靜態變量的時候就創建一個實例。如果按照該方法會過早的創建實例,從而降低內存的使用效率。 

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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類,而該類的作用就是用來釋放資源

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //帶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的析構函數,該析構函數會進行資源的釋放。

Copyright © Linux教程網 All Rights Reserved