设计模式——单例模式(懒汉模式,饿汉模式)
聲明: 本博客參考C語言中文網和優秀博客總結得出:
(1)C語言中文網鏈接
(2)優秀博客鏈接
單例模式的定義: 指一個類只有一個實例,且該類能自行創建這個實例的一種模式。例如,Windows 中只能打開一個任務管理器,這樣可以避免因打開多個任務管理器窗口而造成內存資源的浪費,或出現各個窗口顯示內容的不一致等錯誤。
在計算機系統中,還有 Windows 的回收站、操作系統中的文件系統、多線程中的線程池、打印機的后臺處理服務、應用程序中的對話框、系統中的緩存等常常被設計成單例。
單例模式在現實生活中的應用也非常廣泛,例如公司 CEO、部門經理等都屬于單例模型。
單例模式特點:
(1)單例類只有一個實例對象;
(2)該單例對象必須由單例類自行創建;
(3)單例類對外提供一個訪問該單例的全局訪問點。
為了滿足以上三個特點,我們通常對類進行以下設計:
(1)將構造函數設置為私有。(這樣外界就不可以隨便實例化對象了)
(2)有一個指向實例的靜態指針。
(3)用一個靜態成員方法(或者全局友元函數,只不過我們通常都是用靜態成員方法)來實例化對象,并且對實例化進行控制,只實例化一次,讓靜態指針指向實例,第一次進行實例操作,后面都直接返回已經指向實例的靜態指針變量本身。
單例模式的優點和缺點:
單例模式的優點:
(1)單例模式可以保證內存里只有一個實例,減少了內存的開銷。
(2)可以避免對資源的多重占用。
(3)單例模式設置全局訪問點,可以優化和共享資源的訪問。
單例模式的缺點:
(1)單例模式一般沒有接口,擴展困難。如果要擴展,則除了修改原來的代碼,沒有第二種途徑,違背開閉原則。
(2)在并發測試中,單例模式不利于代碼調試。在調試過程中,如果單例中的代碼沒有執行完,也不能模擬生成一個新的對象。
(3)單例模式的功能代碼通常寫在一個類中,如果功能設計不合理,則很容易違背單一職責原則。
一、懶漢模式
根據代碼看問題:
//單例模式-懶漢模式 class singleton_idler { public:static singleton_idler* Get_objectptr()//靜態成員方法{if (object_ptr == nullptr){mtx.lock();if (object_ptr == nullptr){object_ptr = new singleton_idler;}mtx.unlock();}return object_ptr;}int& Get_val(){return m_val;}private:singleton_idler() {}static singleton_idler* object_ptr;static int m_val;static mutex mtx; };mutex singleton_idler::mtx; int singleton_idler::m_val = 10; singleton_idler* singleton_idler::object_ptr = nullptr;int main() {singleton_idler* ptr1 = singleton_idler::Get_objectptr();cout << "ptr1指向的實例地址為:" << ptr1 << "\t" << "m_val地址為:"<< &(ptr1->Get_val()) << "\t" << "m_val值為:" << ptr1->Get_val() << endl;singleton_idler* ptr2 = singleton_idler::Get_objectptr();cout << "ptr1指向的實例地址為:" << ptr2 << "\t" << "m_val地址為:"<< &(ptr2->Get_val()) << "\t" << "m_val值為:" << ptr2->Get_val() << endl;return 0; }運行結果:
可以通過兩個指針指向的實例的地址看出來,兩個指針指向的是同一個實例,整個過程就只實例化出來一個對象。
這就是懶漢模式,你需要的時候才給你創建一個實例,而且后面你用的就只有這一個實例,創建一個實例后,就不創建了。
構造函數被設置成私有,就是為了讓創建對像的權力只交給下面這個靜態函數:
這個靜態方法是整個懶漢模式的核心,而這個靜態方法的核心就是創建對象,對象的創建被層層保護起來,就是為了確保只實例化一次。
如果object_ptr不為nullptr,說明已經創建過對象了(注意,object_ptr是靜態指針變量),那么就直接返回指針本身就好了。如果object_ptr沒有創建過對象,那么object_ptr就是nullptr(注意代碼中,靜態指針成員變量object_ptr,我們在類外初始化為nullptr了),那么就會進到第一個if語句,而進到第一個if語句加鎖是因為,在多線程的情況下,在沒有實例化對象的情況下,可能兩個地方同時調用這個靜態方法,那么此時兩個地方用的是同一個object_ptr都是nullptr,那么都會進到第一層for循環,所以第一層for循環里面加鎖了,此時只能一個線程拿到鎖,進到里面。那么為什么鎖里面又有個if語句呢? 注意,此時一個線程拿到鎖了,進去了,還一個線程沒有拿到鎖,就阻塞在mtx.lock()這個地方了,當進去的線程實例化完成后,釋放鎖,在mtx.lock()被阻塞的線程就會拿到鎖,如果此時沒有里面那一層if語句,這個線程就會重新new一個對象,將object_ptr重新指向這個對象,這只是兩個線程可能出現的問題,如果更多個線程進到第一個if語句,就會new很多次對象了。當然,如果已經object_ptr已經不為nullptr了,那么沒有進到第一個if語句的線程,第一個if語句就進不去了。所以最安全的辦法就是鎖外鎖內都有一個if語句。
當然,在博客上還可以看到一種寫法:
將鎖外面的if語句去掉了,這樣也是線程安全的,因為鎖只能被一個線程拿到,當第一個實例化對象之后,其他線程拿到鎖進去之后,if語句判斷就會發現object_ptr不為nullptr,就不會再去實例化了。
但是這段代碼和上面兩個if語句的代碼有點區別,就是當已經實例化過對象之后,多個線程同時進到這個靜態方法里面之后,這個時候就會只有一個線程拿到鎖,去判斷if語句,而其他線程就會都阻塞到mtx.lock這里,然后前面的線程拿到鎖判斷完if語句之后,解鎖,被阻塞的這些線程會搶鎖,有一個線程搶到,進入到里面,進行if語句判斷,其他線程阻塞到mtx.lock()這里…
而兩個if語句的代碼,當已經實例化過后,就不存在阻塞到mtx.lock()這里的情況了,因為第一個if語句判斷完不為nullptr之后就直接return object_ptr了。
二、餓漢模式
餓漢模式和單例模式的區別在于,懶漢模式在你需要的時候才去創建,餓漢模式利用靜態指針變量存在數據區的特點,在一開始就直接創建一個實例(在線程創建之前),所以餓漢模式的線程肯定是安全的。
代碼示例:
運行結果:
由于實例在一開始創建,而且在這之后不再可能創建,除非重新編譯運行,為了不讓實例自己析構,我們就將析構函數寫成私有。
將析構函數寫成私有之后,那么實例就只能等程序運行結束,自動回收實例占有的堆空間了。然后如果想要在程序運行結束之前析構實例,就成了難題,(其實我在想,我們將析構函數寫成私有就是為了讓實例在整個程序運行的時候都存在,然后將析構函數寫成私有之后,我們卻又在想怎么析構實例,那么我們為了讓實例在整個程序運行階段都存在而把析構函數寫成私有的意義呢?🤐,但是這些解決辦法都了解了解挺好的),有以下幾點解決辦法:
(1)在類中寫一個釋放資源的方法
運行結果:
(2)定義一個內部類
內部類的對象生存期到了之后,會調用內部類的析構函數,我們在內部類的析構函數中對singleton_hungry的實例進行析構。
運行結果:
(3)利用智能指針
對于這個方法,我實踐了,但是沒有證據證明調用了實例的析構函數進行對象析構了(哪怕在析構函數里做上標記)。
總的來說,對于餓漢模式,我們將析構函數設置成私有,是為了程序運行的整個過程,實例都存在,程序結束,系統會自動回收堆空間,如果說不想要等程序結束自動回收,那就別把析構函數設置成私有了,省的還大費周章的考慮用其他的方式來析構。
一般我們都會將析構函數設置成私有,等程序結束,讓系統自動回收。
博客參考鏈接:優秀博客鏈接
總結
以上是生活随笔為你收集整理的设计模式——单例模式(懒汉模式,饿汉模式)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 单链表——判断两个单链表(无头节点)是否
- 下一篇: 递归算法——汉诺塔问题