如何優雅地實現單例模式?內部靜態類還是雙重檢查鎖定?

在最近的一個項目中,我需要為一個核心配置類實現單例模式。在設計過程中,我發現要同時滿足延遲加載線程安全這兩個要求,常見的實現方式有兩種:內部靜態類雙重檢查鎖定(Double-Checked Locking, DCL)。

起初,我傾向于使用 DCL,它通過雙重檢查來避免不必要的同步開銷,但需要謹慎處理 volatile 關鍵字的使用,確保在多線程環境下的安全性。另一方面,內部靜態類的實現更加簡潔,利用類加載的機制,天然地保證了線程安全和延遲加載。但這兩者在實際應用中各有優劣,那么在面對不同場景時,究竟該如何選擇更合適的單例實現方式呢?

內部靜態類(Bill Pugh Singleton Pattern)

內部靜態類是一種 基于類加載機制 的懶加載實現方式。靜態內部類中的實例只在第一次使用時初始化,JVM 在類加載時會保證這個過程是線程安全的。

靜態內部類不會隨著外部類的加載和初始化而初始化,它只會在被調用時才加載。這利用了 Java 類加載機制的延遲加載特性,同時由 JVM 確保了類的加載過程是線程安全的。

示例代碼

public class Singleton {private Singleton() {}// 靜態內部類,負責持有 Singleton 實例private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}// 獲取 Singleton 實例public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}
優點:
  1. 線程安全:JVM 類加載機制保證了靜態內部類的初始化是線程安全的,避免了顯式的同步控制。
  2. 懶加載:內部靜態類只有在首次調用時才會加載,實現了延遲初始化。
  3. 高效:沒有鎖和同步塊的開銷,性能較好。
  4. 代碼簡單清晰:相比 DCL,代碼結構更簡潔,不易出錯。
缺點:
  • 適用性有限:靜態內部類的方式只能用于單例模式,并且依賴于類加載機制,如果需要實現其他類型的延遲加載或更加復雜的對象初始化流程,可能不適用。
使用場景:
  • 適用于創建單例對象時對性能要求較高的場景,同時需要保證線程安全性。例如,在某些性能敏感的庫或框架中可以使用這種方式來延遲加載資源。

雙重檢查鎖定(Double-Checked Locking, DCL)

雙重檢查鎖定是一種通過手動控制線程同步來實現延遲加載的模式。其核心思想是:在多線程訪問單例時,第一次檢查實例是否為 null,如果是 null,則進入同步代碼塊,再次檢查實例是否為 null,如果依然為 null,才創建實例。這種方式可以減少不必要的同步,提升性能。

DCL 依賴于 volatile 關鍵字來保證線程間的可見性。volatile 確保變量的寫操作對所有線程可見,防止指令重排序帶來的問題(例如對象未完全構造好就被引用)。

示例代碼

public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次檢查synchronized (Singleton.class) {if (instance == null) { // 第二次檢查instance = new Singleton();}}}return instance;}
}
優點:
  1. 延遲加載:和靜態內部類一樣,DCL 也是一種延遲加載的單例實現方式。
  2. 控制更靈活:可以用于更復雜的初始化過程,例如在創建對象時需要加載資源或執行初始化操作。
  3. 節省資源:通過第一次非同步檢查避免了每次獲取實例時的同步開銷,從而提高性能。
缺點:
  1. 實現復雜性:代碼復雜,容易出現錯誤,例如忘記使用 volatile 會導致線程間的可見性問題。
  2. 性能損耗:雖然比直接使用 synchronized 好,但在多核處理器上,由于 volatile 的開銷,性能可能還是會受到影響。
  3. 依賴于 Java 版本:在 Java 5 之前,volatile 沒有確保指令重排序的保障,可能導致雙重檢查鎖定失效。但自 Java 5 起,JVM 對 volatile 的支持增強了,DCL 可以安全使用。
使用場景:

雙重檢查鎖定(DCL)適用于那些需要在多線程環境下延遲加載復雜資源的場景,并且對性能有要求的場景。這里的復雜場景指的是:對象的創建不僅僅是簡單的實例化,而是需要依賴外部資源的加載、進行多步初始化,甚至是需要根據特定條件執行不同的初始化流程。在這種情況下,雙重檢查鎖定可以保證只有在需要時才創建資源,同時確保初始化過程是線程安全的,并避免每次獲取實例時的性能損耗。

示例場景:數據庫連接池的延遲初始化

在某些大型系統中,數據庫連接池的初始化可能非常復雜。假設一個應用程序只有在某些條件滿足時才需要與數據庫交互,為了節省資源,不希望在程序啟動時就立即初始化數據庫連接池,而是希望在第一次需要數據庫時才初始化。

另外,數據庫連接池的創建過程可能包括以下步驟:

  1. 加載配置文件。
  2. 從數據庫驅動程序工廠獲取連接。
  3. 設置各種連接池參數,如最大連接數、超時時間等。
  4. 啟動連接池監控線程。
  5. 其他初始化工作。

由于創建數據庫連接池涉及多個步驟,且初始化過程需要確保只有一個線程能成功創建實例,因此可以使用雙重檢查鎖定來保證線程安全。

示例代碼

public class DatabaseConnectionPool {// 用于保存連接池的單例實例private static volatile DatabaseConnectionPool instance;// 私有的構造方法,防止外部實例化private DatabaseConnectionPool() {// 模擬連接池的復雜初始化過程initializeConnectionPool();}// 獲取連接池實例的靜態方法public static DatabaseConnectionPool getInstance() {if (instance == null) {  // 第一次檢查synchronized (DatabaseConnectionPool.class) {if (instance == null) {  // 第二次檢查instance = new DatabaseConnectionPool();}}}return instance;}// 初始化連接池的方法,假設涉及多個復雜步驟private void initializeConnectionPool() {// 1. 加載數據庫配置loadConfiguration();// 2. 從數據庫驅動程序工廠獲取連接initializeConnections();// 3. 設置連接池參數configurePoolParameters();// 4. 啟動連接池監控線程startConnectionMonitor();// 其他初始化步驟...}private void loadConfiguration() {// 加載數據庫連接的配置信息System.out.println("加載數據庫配置...");}private void initializeConnections() {// 初始化數據庫連接System.out.println("初始化數據庫連接...");}private void configurePoolParameters() {// 設置連接池參數,例如最大連接數、超時設置等System.out.println("配置連接池參數...");}private void startConnectionMonitor() {// 啟動一個后臺線程來監控連接池的健康狀態System.out.println("啟動連接池監控線程...");}// 模擬獲取數據庫連接的方法public void getConnection() {System.out.println("獲取數據庫連接...");}
}

說明:

  1. 延遲加載DatabaseConnectionPool 的實例只有在調用 getInstance() 時才會初始化。這樣,當程序啟動時,如果沒有需要數據庫操作,就不會浪費資源去初始化連接池。

  2. 復雜的初始化過程initializeConnectionPool() 方法模擬了連接池初始化的多個步驟,例如加載配置文件、設置參數、啟動監控線程等。這些步驟需要確保線程安全,因為在多線程環境下,可能有多個線程同時試圖獲取連接池的實例。

  3. 雙重檢查鎖定

    • 第一次檢查:if (instance == null)。如果已經有實例了,就直接返回,避免進入同步塊,從而減少不必要的同步開銷。
    • 第二次檢查:synchronized 塊內的 if (instance == null)。這是為了防止多線程同時通過第一次檢查,確保只有一個線程能創建實例,其他線程將等待第一個線程完成實例化。

場景適用性分析

  1. 資源開銷大:數據庫連接池的創建涉及外部資源的調用、參數的配置,尤其是在高并發場景下,數據庫連接是有限的。每次初始化連接池都需要消耗較多時間和資源,所以使用延遲加載能有效避免不必要的開銷。

  2. 線程安全要求高:由于數據庫連接池是共享的資源,所有線程都會使用同一個連接池實例。如果不保證初始化過程的線程安全,可能會導致多個線程創建多個連接池實例,浪費資源甚至引發沖突。

  3. 多線程訪問:例如,在一個 Web 應用中,多個用戶請求可能同時訪問數據庫。如果在請求高峰期第一次訪問數據庫時并發創建連接池,沒有雙重檢查鎖定,可能會導致多個線程同時初始化連接池,造成不必要的性能損耗。

其他復雜初始化場景

除了數據庫連接池,還有其他一些場景可能適合使用 DCL:

  • 緩存系統的延遲初始化:有時應用程序需要在運行時動態加載緩存數據,初始緩存數據可能需要從外部服務或文件中加載。如果在多個線程同時訪問時沒有同步機制,可能會導致緩存系統加載重復的資源。

  • 配置管理器的延遲加載:在分布式系統中,配置管理器可能需要從多個外部資源加載配置文件(如讀取遠程配置中心、合并本地和遠程配置),這種初始化過程也是多步驟的,且線程安全要求很高。

  • 日志系統的初始化:日志系統的初始化通常涉及創建文件句柄、建立遠程連接、設置格式化器等多步驟操作。在高并發環境下,多個線程同時訪問日志系統時,也需要確保日志系統的初始化是安全且高效的。


內部靜態類 vs 雙重檢查鎖定的對比

特性靜態內部類雙重檢查鎖定(DCL)
實現復雜性簡單,代碼清晰復雜,容易出錯
線程安全JVM 保證線程安全需要手動保證(volatile
延遲加載
性能開銷無鎖開銷,性能高volatile 和鎖開銷
適用場景適合單例模式適合更復雜的初始化場景
JVM 依賴無需依賴 volatile依賴 volatile 和 Java 版本
擴展性一般,適合單例模式靈活,適合復雜的延遲初始化

場景選擇

  • 靜態內部類:適合在需要高效、簡潔且線程安全的場景下實現單例模式,且不需要復雜的初始化流程。這種方式通常是推薦的單例實現方式,特別是性能要求較高的場合。
  • 雙重檢查鎖定:適合需要復雜初始化邏輯的場景,或者在一些特殊的情況下,可能會涉及到需要動態控制單例對象的創建流程。盡管代碼復雜性較高,但在 Java 5 之后的版本中,已經可以安全地使用雙重檢查鎖定。

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

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

相關文章

【計算機網絡】 —— 數據鏈路層(壹)

文章目錄 前言 一、概述 1. 基本概念 2. 數據鏈路層的三個主要問題 二、封裝成幀 1. 概念 2. 幀頭、幀尾的作用 3. 透明傳輸 4. 提高效率 三、差錯檢測 1. 概念 2. 奇偶校驗 3. 循環冗余校驗CRC 1. 步驟 2. 生成多項式 3. 例題 4. 總結 四、可靠傳輸 1. 基本…

golang實現簡單的redis服務

golang 手搓redis服務器倉庫地址:實現思路: golang 手搓redis服務器 倉庫地址: 倉庫: https://github.com/dengjiayue/my-redis.git 實現思路: ● 協議: tcp通信 ● 數據包: 長度(4byte)方法(1byte)數據json ● 數據處理: 單線程map讀寫 ○ 依次處理待處理隊列的請求(chan)…

智慧銀行反欺詐大數據管控平臺方案(八)

智慧銀行反欺詐大數據管控平臺的核心理念,在于通過整合先進的大數據技術、算法模型和人工智能技術,構建一個全面、智能、動態的反欺詐管理框架,以實現對金融交易的全方位監控、欺詐行為的精準識別和高效處理。這一理念強調數據驅動決策&#…

3D 生成重建019-LERF用文本在Nerf中開啟上帝之眼

3D 生成重建019-LERF用文本在Nerf中開啟上帝之眼 文章目錄 0 論文工作1 論文方法2 實驗結果 0 論文工作 人類利用自然語言描述物理世界,根據各種特性(視覺外觀、語義、抽象關聯)尋找具體的3D位置。在這項工作中,作者提出了語言嵌…

如何選擇合適的期刊投稿?從課題組經驗到在線工具的使用全解析

~~~本文是作者個人的經驗分享,建立在導師讓自己選刊的情況下~~~ 投稿選刊是科研過程中至關重要的一步,選刊過程可能讓許多初投稿的研究者感到迷茫和困惑:期刊那么多,如何找到最合適的? 本文將從多個角度介紹如何選擇投…

024、Docker與SSH在分布式系統中的實踐指南

1. Docker SSH配置最佳實踐 Docker容器通常不需要SSH服務來運行,因為它們設計為輕量級、無狀態的,并且通常通過Docker命令行界面與宿主機進行交互。但是,在某些情況下,您可能需要通過SSH訪問Docker容器進行調試、維護或其他操作。…

【kafka】消息隊列的認識,Kafka與RabbitMQ的簡單對比

什么是消息隊列? 消息隊列(Message Queue,簡稱 MQ)是一個在不同應用程序、系統或服務之間傳遞數據的機制。 它允許系統間異步地交換信息,而無需直接交互,確保消息的可靠傳輸。 想象一下,你正在…

.NET MAUI與.NET for Android/IOS的關系

2024年11月13日微軟發布了.Net9.0,我打算體驗一下。安裝好.Net9.0 SDK后發現Visual Studio識別不到9.0,但是通過命令行dotnet --info查看是正常的,后面看到了VS有版本可以升級,把VS升級到17.12.0就可以了。更新完打開以后看到如下界面 這里…

SqlDataAdapter

SqlDataAdapter 是 .NET Framework 和 .NET Core 中提供的一個數據適配器類,屬于 System.Data.SqlClient 命名空間(或在 .NET 6 中屬于 Microsoft.Data.SqlClient 命名空間)。它的作用是充當數據源(如 SQL Server 數據庫&#xff…

【vivado】時序報告--best時序和worst時序

利用vivado進行開發時,生成best時序報告和worst時序報告。 best時序報告 slow選擇min_max,fast選擇none。 worst時序報告 fast選擇min_max,slow選擇none。

FastAPI 響應狀態碼:管理和自定義 HTTP Status Code

FastAPI 響應狀態碼:管理和自定義 HTTP Status Code 本文介紹了如何在 FastAPI 中聲明、使用和修改 HTTP 狀態碼,涵蓋了常見的 HTTP 狀態碼分類,如信息響應(1xx)、成功狀態(2xx)、客戶端錯誤&a…

力扣題庫-擲骰子模擬詳細解析

題目如下: 有一個骰子模擬器會每次投擲的時候生成一個 1 到 6 的隨機數。 不過我們在使用它時有個約束,就是使得投擲骰子時,連續 擲出數字 i 的次數不能超過 rollMax[i](i 從 1 開始編號)。 現在,給你一…

深入淺出:PHP中的數據類型全解析

文章目錄 引言理解數據類型標量類型整數 (integer)浮點數 (float)布爾值 (boolean)字符串 (string) 復合類型數組 (array)對象 (object)資源 (resource)NULL 特殊類型Callable強制類型轉換 實戰案例總結與展望參考資料 引言 在編程的世界里,數據類型是構建任何應用…

當linux可執行文件缺少或者不兼容so庫時候,如何查看版本以及缺少那些庫

解決方法: ldd 命令來驗證程序是否加載了正確的庫: 如檢查linear_elasticity可執行文件缺少的庫,用下面命令: ldd linear_elasticity 可以發現下面not found就是缺少的庫,還有對應的庫的位置已經版本 $ ldd lin…

第P1周:Pytorch實現mnist手寫數字識別

🍨 本文為🔗365天深度學習訓練營 中的學習記錄博客🍖 原作者:K同學啊 目標 1. 實現pytorch環境配置 2. 實現mnist手寫數字識別 3. 自己寫幾個數字識別試試具體實現 (一)環境 語言環境:Python…

Seq2Seq模型的發展歷史;深層RNN結構為什么出現梯度消失/爆炸問題,Transformer為什么不會;Seq2Seq模型存在問題

目錄 Seq2Seq模型的發展歷史 改進不足的地方 深層RNN結構為什么出現梯度消失/爆炸問題,Transformer為什么不會 深層RNN結構為什么出現梯度消失/爆炸問題: Transformer為什么不會出現梯度消失/爆炸問題: Seq2Seq模型存在問題 T5模型介紹 Seq2Seq模型的發展歷史 序列到…

網絡安全技術詳解:虛擬專用網絡(VPN) 安全信息與事件管理(SIEM)

虛擬專用網絡(VPN)詳細介紹 虛擬專用網絡(VPN)通過在公共網絡上創建加密連接來保護數據傳輸的安全性和隱私性。 工作原理 VPN的工作原理涉及建立安全隧道和數據加密: 隧道協議:使用協議如PPTP、L2TP/IP…

Hive 窗口函數與分析函數深度解析:開啟大數據分析的新維度

Hive 窗口函數與分析函數深度解析:開啟大數據分析的新維度 在當今大數據蓬勃發展的時代,Hive 作為一款強大的數據倉庫工具,其窗口函數和分析函數猶如一把把精巧的手術刀,助力數據分析師們精準地剖析海量數據,挖掘出深…

SCAU期末筆記 - 數據庫系統概念

我校使用Database System Concepts,9-12章不考所以跳過,因為課都逃了所以復習很倉促,只準備過一下每一章最后的概念辨析,我也不知道有沒有用 第1章 引言 數據庫管理系統(DBMS) 由一個互相關聯的數據的集合…

Android 12系統源碼_窗口管理(九)深淺主題切換流程源碼分析

前言 上一篇我們簡單介紹了應用的窗口屬性WindowConfiguration這個類,該類存儲了當前窗口的顯示區域、屏幕的旋轉方向、窗口模式等參數,當設備屏幕發生旋轉的時候就是通過該類將具體的旋轉數據傳遞給應用的、而應用在加載資源文件的時候也會結合該類的A…