常見的設計模式(2)單例模式

目錄

一、版本一:禁用構造與拷貝

二、版本二:注冊析構函數/嵌套垃圾回收

(1)使用atexit注冊程序結束時的函數

(2)使用對象嵌套垃圾回收

三、版本三:線程安全

四、版本四:編譯器、CPU指令重排問題

五、版本五:局部靜態變量線程安全性

六、版本六:模板提高復用率


????????單例模式是C++中最常用的一種設計模式,他確保無論在單線程還是多線程中,都只會有一個實例對象,并提供一個全局訪問點。這種模式在配置管理,日志記錄,設備驅動等場景中非常有用。本文將從思維迭代的方式,一步步完善單例模式,方便大家理解和記憶。

? ? ? ? 分析一個設計模式,通常要從穩定點和變化點入手。對于單例模式其穩定點顯然是一個類要提供一個一個全局的訪問方式,且不允許外部任意構造、拷貝。而變化點理論上是沒有的,但是我們強行認為變化點在于使用繼承+模板來擴展單例模式。

一、版本一:禁用構造與拷貝

????????既然單例模式只能提供一個全局訪問點,且不能讓其他人隨意創建,那么很顯然需要禁用其構造函數、拷貝構造等。當然這里是懶漢模式,如果你采取餓漢模式,直接在靜態區創建單例對象,而非創建單例指針,則可以避免析構調不到的問題。下面我們來分析一下:

class Singleton
{
public://獲取全局訪問點static Singleton* GetInstance(){if (_instance==nullptr){_instance = new Singleton();}return _instance;}private:static Singleton* _instance;	//全局訪問點//私有化各種構造函數,防止外面任意創建對象
private:Singleton() {};		//構造~Singleton() {};	//析構Singleton(const Singleton&) = delete;		//拷貝構造Singleton& operator=(const Singleton&) = delete;		//賦值運算符重載Singleton(Singleton&&) = delete;		//移動構造Singleton& operator=(Singleton&&) = delete;		//移動賦值運算符重載
};
Singleton* Singleton::_instance = nullptr;	//初始化靜態成員

????????我們可以看到這個代碼存在一些問題。比如他不能自動調用析構函數(即使我們把析構函數public,再手動調用也會出現信號等情況沒有執行到這里就退出了),因為單例對象的雖然是創建在堆上的,但是其指針在全局靜態區。

????????當程序聲明周期到達、或者以外收到信號退出的時候,該進程的地址空間雖然會被操作系統回收,僅僅會對這個指針銷毀,無法析構其指向的內容(堆上的對象必須要手動調用delete才會被析構)。

? ? ? ? 既然無法調用到析構函數,那么其析構的執行流也無法被執行。當他的析構函數涉及到文件操作、網絡連接等資源。比如關閉文件描述符、刷新文件緩沖區到內核態時就會出問題。舉個例子:日志對象是一個單例對象,他打開了一系列文件,正常情況下手動調用析構函數會正常關閉文件描述符,而關閉文件描述符是一個把用戶態文件緩沖區刷新到內核態的步驟,如果沒有close文件描述符,操作系統會直接回收資源,并不管你用戶態的緩沖區是否有數據沒有刷新,即你丟失了這部分數據。

二、版本二:注冊析構函數/嵌套垃圾回收

? ? ? ? 既然版本一存在這種明顯的無法正確析構的問題。而在c庫中有一個atexit,它可以向操作系統注冊一個函數,該函數僅會在程序正常終止時被調用

(1)使用atexit注冊程序結束時的函數

class Singleton
{
public://獲取全局訪問點static Singleton* GetInstance(){if (_instance == nullptr){_instance = new Singleton();atexit(Destructor);}return _instance;}private:static void Destructor(){if (_instance != nullptr){delete _instance;_instance = nullptr;}}private:static Singleton* _instance;	//全局訪問點//私有化各種構造函數,防止外面任意創建對象
private:Singleton() {};		//構造~Singleton() {};	//析構Singleton(const Singleton&) = delete;		//拷貝構造Singleton& operator=(const Singleton&) = delete;		//賦值運算符重載Singleton(Singleton&&) = delete;		//移動構造Singleton& operator=(Singleton&&) = delete;		//移動賦值運算符重載
};
Singleton* Singleton::_instance = nullptr;	//初始化靜態成員

(2)使用對象嵌套垃圾回收

????????利用GarbageCollector靜態全局對象在程序正常結束的時候,會自動調用其析構函數,而在他的析構函數中又調用了單例對象的析構函數,從而完成回收。簡單來說就是利用了智能指針RAII的思路。

class Singleton {
private:static Singleton* _instance;// 嵌套垃圾回收類class GarbageCollector {public:~GarbageCollector() {if (Singleton::_instance != nullptr) {delete Singleton::_instance;Singleton::_instance = nullptr;}}};static GarbageCollector _gc; // 全局靜態成員,程序結束時自動析構Singleton() {std::cout << "Singleton created" << std::endl;}~Singleton() {std::cout << "Singleton destroyed" << std::endl;}public:static Singleton* GetInstance() {if (_instance == nullptr) {_instance = new Singleton();}return _instance;}// 禁用拷貝和移動操作Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;Singleton(Singleton&&) = delete;Singleton& operator=(Singleton&&) = delete;
};Singleton* Singleton::_instance = nullptr;
Singleton::GarbageCollector Singleton::_gc;

三、版本三:線程安全

? ? ? ? 雖然版本二在單線程場景下已經足夠使用。但在多線程情況,卻會出現重復走到if,然后創建多個對象的競態問題。

? ? ? ? 這個代碼中使用到了雙重檢測機制。即使在沒有創建單例對象的時候,多個線程進入了第一個if里面,然后會因為鎖競爭只能有一個線程執行到第二個if里面去創建單例對象。當他釋放鎖后,別的線程會繼續競爭鎖并判斷是否為nullptr。如果為空則退出。

? ? ? ? 所以在這個代碼中,只會有第一次n個線程進入if后的n次加鎖、解鎖開銷。

// 雙重檢查鎖定(DCL)
class Singleton 
{
private:static Singleton* instance;static std::mutex mtx;Singleton() {}public:static Singleton* getInstance() {if (instance == nullptr) {  // ① 第一次檢查(無鎖)std::lock_guard<std::mutex> lock(mtx);  // 加鎖if (instance == nullptr) {  // ② 第二次檢查(有鎖)instance = new Singleton();atexit(Destructor);//	向操作系統注冊析構函數}}return instance;}private:static void Destructor(){if (instance != nullptr){delete instance;instance = nullptr;}}
private://禁用各種構造Singleton() {};		//構造~Singleton() {};	//析構Singleton(const Singleton&) = delete;		//拷貝構造Singleton& operator=(const Singleton&) = delete;		//賦值運算符重載Singleton(Singleton&&) = delete;		//移動構造Singleton& operator=(Singleton&&) = delete;		//移動賦值運算符重載
};

四、版本四:編譯器、CPU指令重排問題

????????解決了多線程競態問題后,發現編譯器、CPU會按照單線程的執行思想,自以為是的優化執行順序,這就導致了new本身可能亂序。

? ? ? ? new操作符在底層會分為三個步驟:

????????其中operator new是基于內存池的,所以他是線程安全的。而構造對象這一步是程序員手動執行的,既不線程安全,執行順序也不能保證。

編譯器或 CPU 為了優化性能,可能把步驟 3 調整到步驟 2 之前,變成:

所以我們需要使用內存屏障來保證執行流的可見性問題。

同時由于對普通指針?instance?的讀寫不是原子操作。在多線程環境下,可能出現線程 A 寫入指針的 “中間狀態”(比如只更新了低 32 位),線程 B 讀取時拿到一個無效的指針值,直接崩潰。所以用原子操作解決原子性問題。

class Singleton 
{
public:static Singleton* getInstance() {Singleton* tmp = instance.load(std::memory_order_acquire);  // 讀操作if (tmp == nullptr) {std::lock_guard<std::mutex> lock(mtx);tmp = instance.load(std::memory_order_relaxed);if (tmp == nullptr) {tmp = new Singleton();// 寫操作:禁止重排,保證構造完成后再賦值instance.store(tmp, std::memory_order_release);  }}return tmp;}
private:// 用 atomic 修飾指針,禁止指令重排static std::atomic<Singleton*> instance;  static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mtx;

關于這里的內存屏障、原子操作只需要大致認識即可,后續會有文章詳細講解。

? ? ? ? 雖然這種方式已經足夠,但寫起來太過繁瑣。

五、版本五:局部靜態變量線程安全性

? ? ? ? 在C++11后規定magic static的特性:

  • 局部靜態變量(如?static Singleton instance)的初始化是線程安全的。若多個線程同時首次調用?GetInstance(),編譯器會保證只有一個線程執行變量初始化,其他線程會阻塞等待初始化完成后再訪問,無需手動加鎖。即保證了線程安全性,又保證了可見性問題。
  • 自動銷毀:程序結束時,局部靜態變量會按構造的逆序自動銷毀,調用?~Singleton()?釋放資源。
  • 注意:只有局部靜態變量才能這么做,如果是全局靜態變量則未被標準保證,仍需使用之前的方式。
#include <iostream>
// 如需線程安全驗證,可包含此頭文件(C++11及以上環境)
#include <thread>  class Singleton {
public:// 核心:局部靜態變量,C++11后保證線程安全初始化//并使用&來保證訪問效率    static Singleton& GetInstance() {static Singleton instance;  // 第一次調用時初始化,后續直接返回引用return instance;}// 示例:單例的業務方法void DoSomething() const {std::cout << "Singleton is working, address: " << this << std::endl;}private:// 1. 私有構造:禁止外部直接創建Singleton() {std::cout << "Singleton constructed." << std::endl;}// 2. 私有析構:禁止外部直接銷毀(由系統自動調用)~Singleton() {std::cout << "Singleton destructed." << std::endl;}// 3. 禁用拷貝語義:防止對象復制Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;// 4. 禁用移動語義:防止對象移動Singleton(Singleton&&) = delete;Singleton& operator=(Singleton&&) = delete;
};

????????這種方式也是我們最推薦的寫法,他即不用考慮無法自動析構導致資源泄露的問題,也不用考慮線程安全,最后甚至不需要考慮CPU編譯器的指令重排,可以說局部靜態變量的標準出現,讓單例模式得到了顯著的進步。

????????但有人說,這樣你每寫一個單例類就需要手動禁用一下其構造函數等等,還是稍顯麻煩,那么我們下面的寫法則將他封裝成了一個基類。

六、版本六:模板提高復用率

????????當父類的各種構造被禁用了,子類想要調用對應的構造,首先會調用父類的,然后發現錯誤,實現單例模式,且不需要在子類手動禁用。

// 單例模式基類模板
template <typename T>
class Singleton {
public:// 禁用拷貝構造Singleton(const Singleton&) = delete;// 禁用拷貝賦值Singleton& operator=(const Singleton&) = delete;// 禁用移動構造Singleton(Singleton&&) = delete;// 禁用移動賦值Singleton& operator=(Singleton&&) = delete;// 獲取單例實例static T& getInstance() {// 靜態局部變量,C++11后保證線程安全初始化static T instance;return instance;}protected:// 保護的構造函數,允許子類構造Singleton() = default;// 保護的析構函數,允許子類析構virtual ~Singleton() = default;
};

當你使用的時候,只需要繼承于該基類,然后重寫其中的構造函數、析構函數即可,舉個例子:

// 1. 日志管理器 - 單例應用場景
class Logger : public Singleton<Logger> {friend class Singleton<Logger>;
private:// 私有構造函數,初始化日志系統Logger() {std::cout << "Logger initialized. Starting to log messages..." << std::endl;}// 私有析構函數,清理日志系統~Logger() {std::cout << "Logger shutting down. Finalizing log files..." << std::endl;}public:// 日志級別enum class Level { INFO, WARNING, ERROR };// 記錄日志的方法void log(const std::string& message, Level level = Level::INFO) {// 簡單的線程安全處理std::lock_guard<std::mutex> lock(mtx);// 根據級別輸出不同前綴std::string prefix;switch(level) {case Level::INFO:    prefix = "[INFO]   "; break;case Level::WARNING: prefix = "[WARNING]"; break;case Level::ERROR:   prefix = "[ERROR]  "; break;}// 輸出日志信息std::cout << prefix << message << std::endl;}private:std::mutex mtx; // 確保日志輸出線程安全
};

????????這里可以看到他引入了一個友元類,讓基類可以訪問到子類的私有構造、析構函數。在之前的設計模式中由于子類重寫的函數都是public的,所以不需要友元。這一點需要注意一下。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/92875.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/92875.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/92875.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

JAiRouter 0.2.1 更新啦:內存優化 + 配置合并 + IP 限流增強,運維體驗再升級

JAiRouter 0.2.1 更新啦&#xff1a;內存優化 配置合并 IP 限流增強&#xff0c;運維體驗再升級 如果你已經在 0.2.0 生產環境中穩定運行&#xff0c;那么這篇更新會讓你無痛升級&#xff0c;直接“更輕、更穩、更省心”。 &#x1f4ce; 官方倉庫 & issue 直達 https://…

學習嵌入式第二十六天

文章目錄IO(續上)1.標準IO1.標準IO的接口2.流的定位2.文件IO1.概念&#xff1a;2.系統調用和庫函數3.文件IO函數接口習題IO(續上) 1.標準IO 1.標準IO的接口 fwrite 原型&#xff1a;size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream); 功能&#xff1…

GDB 程序啟動參數設置深度指南

GDB 程序啟動參數設置深度指南 1. 概述 在程序調試過程中&#xff0c;正確設置啟動參數對于驗證程序行為、重現特定場景至關重要。GDB提供多種靈活的方式設置啟動參數&#xff0c;特別是當您需要調試命令行參數處理邏輯或配置敏感型應用時。 2. 參數設置的核心方法 2.1 啟動GDB…

Autudl華為昇騰系列NPU簡介和部署推理yolo11 yolov8 yolov5分割模型

0.配置Autudl 下面圖片是我所租的昇騰卡和具體環境版本&#xff0c;太具體的就不說了&#xff0c;有需要的話我單獨出一期Autudl租顯卡的教程&#xff0c;主要是為了學習昇騰環境如何運行Yolo系列模型。 0.1華為昇騰芯片&#xff08;Ascend&#xff09;簡介 1.Ascend 310&…

什么是JSP和Servlet以及二者的關系

JSP&#xff08;JavaServer Pages&#xff09; 是“HTML 里寫 Java”的模板技術&#xff0c;最終會被容器轉換成 Servlet。Servlet 是“Java 里寫 HTML”的 Java 類&#xff0c;直接繼承 javax.servlet.http.HttpServlet&#xff0c;用來接收/響應 HTTP 請求。Servlet 是什么 純…

【WonderTrader源碼詳解 1】【環境搭建 2】【編譯安裝WonderTrader】

一、引言 本篇來講述如何搭建 wondertrader 和 wtpy 二、wondertrader 2.1 源碼下載 # /home/leo/sda_1.6TBgit clone https://gitee.com/wondertrader/wondertrader.gitgit clone https://gitee.com/wondertrader/wtpy.git2.2 源碼編譯 cd /home/leo/sda_1.6TB/wondertrader/s…

hutool 作為http 客戶端工具調用的一點點總結

場景一&#xff1a;客戶端timeout 的時間給的很短//100毫秒 private static final int HTTP_TIMEOUT_MS 1 * 100; response HttpUtil.createPost(patrolresultconfirmUrl).body(JSONObject.toJSONString(search)).header("Authorization", token).timeout(HTTP_TI…

基于MongoDB/HBase的知識共享平臺的設計與實現

標題:基于MongoDB/HBase的知識共享平臺的設計與實現內容:1.摘要 在當今信息爆炸的時代&#xff0c;知識的有效共享和管理變得愈發重要。本研究的目的是設計并實現一個基于MongoDB/HBase的知識共享平臺&#xff0c;以滿足大規模知識數據的存儲、高效查詢和快速共享需求。方法上&…

PHP數組操作:交集、并集和差集

1. 交集&#xff08;Intersection&#xff09;交集是指兩個集合中都存在的元素。$array1 [1, 2, 3, 4]; $array2 [3, 4, 5, 6];$intersection array_intersect($array1, $array2); print_r($intersection); // 輸出: Array ( [2] > 3 [3] > 4 )2. 并集&#xff08;Uni…

Qt 常用控件 - 7

Text Edit&#xff08;多行輸入框&#xff09;QTextEdit 表示多行輸入框&#xff0c;也是一個富文本 & markdown 編輯器&#xff0c;能在內容超出范圍時自動提供滾動條。QTextEdit&#xff1a;不僅僅能表示純文本&#xff0c;還可以表示 htnl 和 markdownQPlainTextEdit&am…

JDK、eclipse的安裝,配置JDK、Tomcat并使用eclipse創建項目

目錄一、JDK的安裝1. 安裝JDK2. 配置環境變量3. 檢查jdk是否已安裝二、eclipse的安裝1. 解壓安裝2. 設置字體大小3. 設置拼寫提示三、tomcat安裝四、創建項目1. 第一次創建一個普通的java項目2. 第一次創建一個java web項目擴展&#xff1a;運行項目報Tomcat端口占用&#xff0…

Iptables 詳細使用指南

目錄 1. 工作原理? 2. 核心架構&#xff08;四表五鏈&#xff09; 2.1 四張表&#xff08;優先級從高到低&#xff09; 2.2 五條內置鏈&#xff08;數據包流向&#xff09; 3. Iptables規則 3.1 規則的匹配條件與目標動作 常見匹配條件&#xff08;用于篩選數據包&…

Vue 服務端渲染(SSR)詳解

Vue SSR是一種在服務端將 Vue 應用渲染成 HTML 字符串&#xff0c;然后直接發送到客戶端的技術。相比傳統的客戶端渲染&#xff0c;Vue SSR 能帶來更好的 SEO 性能和更快的首屏加載時間。下面我們從零到一&#xff0c;結合項目源碼&#xff0c;詳細講解如何實現一個 Vue SSR 項…

機器翻譯:需要了解的數學基礎詳解

文章目錄一、概率論與統計學1.1 基本概念1.2 在機器翻譯中的應用二、線性代數2.1 基本概念2.2 在機器翻譯中的應用三、微積分3.1 基本概念3.2 在機器翻譯中的應用四、信息論4.1 基本概念4.2 在機器翻譯中的應用五、數值優化5.1 優化問題形式化5.2 優化算法5.3 正則化技術六、圖…

藍橋杯手算題和雜題簡易做法

一、巧用Excel Excel在解決某些數學問題時非常高效&#xff0c;特別是涉及表格計算、簡單統計和可視化分析時。 門牌制作 這道題是一道基礎題&#xff0c;只需要判斷每個數字有幾個2&#xff0c;然后在加起來即可&#xff0c;但是還有更簡單的方法&#xff0c;先通過編譯器&…

5. 緩存-Redis

文章目錄前言一、 介紹1. 簡介2. 核心特點二、 應用場景1. 應用場景2. 數據類型作用場景三、 性能特性1. 內存2. 高性能數據結構3. 單線程、多路復用四、 異步持久化機制1. RDB&#xff08;Redis Database&#xff09;2. AOF&#xff08;Append-Only File&#xff09;3. 持久化…

如何理解Tomcat、Servlet、Catanalina的關系

目錄 背景&#xff1a; 結論&#xff1a; 好文-【拓展閱讀】&#xff1a; 象漂亮更新動力&#xff01; 背景&#xff1a; 學習Java的Servlet時&#xff0c;常常說Tomcat是一個容器&#xff0c;我們寫ServletA,ServletB,Tomcat容器在啟動的時候會讀取web.xml或者我們程序中的…

Hive的并行度的優化

對于分布式任務來說&#xff0c;任務執行的并行度十分重要。Hive的底層是MapReduce&#xff0c;所以Hive的并行度優化分為Map端優化和Reduce端優化。(1)、Map端優化Map端的并行度與Map切片數量相關&#xff0c;并行度等于切片數量。一般情況下不用去設置Map端的并行度。以下特殊…

Vue.js 響應接口:深度解析與實踐指南

Vue.js 響應接口&#xff1a;深度解析與實踐指南 引言 隨著前端技術的不斷發展&#xff0c;Vue.js 作為一種流行的前端框架&#xff0c;已經成為了眾多開發者的首選。Vue.js 的響應式系統是其核心特性之一&#xff0c;它允許開發者輕松實現數據的雙向綁定。而響應接口則是Vue.j…

高精度藍牙定位:技術、應用與未來發展

一、高精度藍牙定位概述在當今科技飛速發展的時代&#xff0c;定位技術的精度和可靠性變得越來越重要。高精度藍牙定位作為一種新興的定位技術&#xff0c;正逐漸嶄露頭角。藍牙技術是一種支持設備短距離通信&#xff08;一般10m內&#xff09;的無線電技術&#xff0c;能在包括…