【JUC】深入解析 JUC 并發編程:單例模式、懶漢模式、餓漢模式、及懶漢模式線程安全問題解析和使用 volatile 解決內存可見性問題與指令重排序問題

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述


單例模式


單例模式確保某個類在程序中只有一個實例,避免多次創建實例(禁止多次使用new)。

要實現這一點,關鍵在于將類的所有構造方法聲明為private

這樣,在類外部無法直接訪問構造方法,new操作會在編譯時報錯,從而保證類的實例唯一性。例如,在JDBC中,DataSource實例通常只需要一個,單例模式非常適合這種場景。

單例模式的實現方式主要有兩種:“餓漢式”和“懶漢式”


餓漢模式


img


下面這段代碼,是對唯一成員 instance 進行初始化,用 static 修飾 instance,對 instance 的初始化,會在類加載的階段觸發;類加載往往就是在程序一啟動就會觸發;img

由于是在類加載的階段,就早早地創建好了實例(static修飾),這也就是“餓漢模式” 名字的由來。


在初始化好 instance 后,后續統一通過調用 getInstance() 方法獲取 instance

img


單例模式的“點睛之筆”,用 private 修飾類中所有構造方法,因為可以防止通過 new 關鍵字在類外部創建實例,只能通過調用內部靜態方法,來獲取單例類實例:

img


img


懶漢模式


  • 餓漢模式在類加載時即創建實例,通過將構造方法聲明為private,防止外部創建其他實例。
  • 懶漢模式:延遲創建實例,僅在真正需要時才創建。這種模式在某些情況下無需實例對象時,可避免不必要的實例化,減少開銷并提升效率。

單線程版本


在懶漢模式下,實例的創建時機是在第一次被使用時,而不是在程序啟動時。

如果程序啟動后立即需要使用實例,那么懶漢模式和餓漢模式的效果相似。

然而,如果程序運行了較長時間仍未使用該實例,懶漢模式會延遲實例的創建,從而減少不必要的開銷

img


多線程版本


img


單例模式產生線程安全的原因


img


餓漢模式


img


懶漢模式


為什么會有單線程版本和多線程版本的懶漢模式寫法呢?我們來看單線程版本,如果運用到多線程的環境下,會出現什么問題:

img

在懶漢模式中,instance被聲明為static,因此多個線程調用getInstance()時,返回的是同一個實例。

然而,getInstance()方法中既包含讀操作(檢查instance是否為null),也包含寫操作(實例化instance)。

盡管賦值操作本身是原子的,但整個getInstance()方法并非原子操作。這意味著在多線程環境下,判斷和賦值操作不能保證緊密執行,從而導致線程安全問題。

img

在多線程環境下,若兩個線程(如 t1 和 t2)同時執行 getInstance() 方法,可能會導致值覆蓋問題。

如上圖,t2 線程的賦值操作可能會覆蓋 t1 線程新創建的對象,導致第一個線程創建的對象被垃圾回收(GC)

這不僅增加了不必要的開銷,還違背了單例模式的核心目標:避免重復創建實例,減少耗時操作,節省資源。即使第一個對象很快被釋放,其創建過程中的數據加載依然會產生額外開銷。


總結:

  • 餓漢模式:僅涉及對實例的讀操作,不涉及寫操作,因此天然線程安全。無論在單線程還是多線程環境下,其基本形式保持不變。
  • 懶漢模式:在getInstance()中包含緊密相關的讀寫操作(檢查實例是否存在并創建實例),但這些操作無法緊密執行,導致線程安全問題。

解決單例模式的線程安全問題


面試題:

這兩個單例模式的 getInstance() 在多線程環境下調用,是否會出現 bug,如何解決 bug?


1. 通過加鎖讓讀寫操作緊密執行


餓漢模式本身不存在線程安全問題,因為它僅涉及讀操作,不涉及寫操作。

然而,懶漢模式在多線程環境下可能出現線程安全問題,原因在于getInstance()方法中的讀寫操作(判斷 + 賦值)不能緊密執行。

為解決這一問題,需要對相關操作進行加鎖,以確保線程安全。


方法一:對方法中的讀操作加鎖


img

這樣加鎖后,如果 t1 和 t2 還出現下圖讀寫邏輯的執行順序:

img

  • t2 會阻塞等待 t1(或 t1 等待 t2)完成對象的創建(讀寫操作結束后),釋放鎖后,第二個線程才能繼續執行。
  • 此時,第二個線程發現 instance 已非 null,會直接返回已創建的實例,不再重復創建。

方法二:對整個方法加鎖


img

直接對getInstance()方法加鎖,也能確保讀寫操作緊密執行。此時,鎖對象為SingletonLazy.class。這兩種方法的效果相同


2. 處理加鎖引入的新問題


問題描述


對于當前懶漢模式的代碼,多個線程共享一把鎖,不會導致死鎖。只需確保第一個線程調用getInstance()時,讀寫操作緊密執行即可。

后續線程在讀取時發現instance != null就不會觸發寫操作,從而自然保證了線程安全。

img


然而,若每次調用getInstance()方法時都進行加鎖解鎖操作,由于synchronized是重量級鎖,多次加鎖,尤其是重量級鎖會導致顯著的性能開銷,從而降低程序效率

img

拓展:


StringBuffer 就是為了解決,大量拼接字符串時,產生很多中間對象問題而提供的一個類,提供 appendinsert 方法,可以將字符串添加到,已有序列的 末尾 或 指定位置。


StringBuffer 的本質是一個線程安全的可修改的字符序列,把所有修改數據的方法都加上了synchronized。但是保證了線程安全是需要性能的代價的。


在很多情況下我們的字符串拼接操作,不需要線程安全,這時候 StringBuilder 登場了,
StringBuilder 是 JDK1.5 發布的, StringBuilderStringBuffer 本質上沒什么區別,就是去掉了保證線程安全的那部分,減少了開銷。所以在單線程情況下,優先考慮使用 StringBuilder


StringBufferStringBuilder 二者都繼承了 AbstractStringBuilder,底層都是利用可修改的 char數組 (JDK9以后是 byte 數組)。


所以如果我們有大量的字符串拼接,如果能預知大小的話最好在new StringBuffer 或者 new StringBuilder 的時候設置好 capacity ,避免多次擴容的開銷(擴容要拋棄原有數組,還要進行數組拷貝創建新的數組)。


解決方法


再嵌套一次判斷操作,既可以保證線程安全,又可以避免大量加鎖解鎖產生的開銷:

img

在單線程環境下,嵌套兩層相同的if語句并無意義,因為單線程只有一個執行流,嵌套與否結果相同。但在多線程環境下,多個并發執行流,可能導致不同線程在執行判斷操作時,因其他線程修改了instance而得到不同結果。

例如,在懶漢模式下,即使兩個if語句形式相同,其目的和作用卻不同

  • 第一個if用于判斷是否需要加鎖;
  • 第二個if用于判斷是否需要創建對象。

這種結構雖看似巧合,但實則必要。


3. 引入 volatile 關鍵字


問題描述


懶漢模式的單例實現中,使用volatile關鍵字修飾instance至關重要。以下是懶漢模式的單例實現代碼:

private static SingletonLazy instance = null;public static SingletonLazy getInstance() {if (instance == null) {synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}}}return instance;
}

如果不使用volatile修飾instance,可能會出現以下問題:


內存可見性問題

核心問題

  1. 在沒有 volatile 修飾時,線程 t1instance寫入可能僅停留在線程本地緩存(CPU緩存或寄存器),而非立即同步到主內存
  2. 此時線程 t2 讀取的可能是自己緩存中的舊值(null),即使 t1 已完成初始化。
  3. 即使 t2 進入同步塊,第一次判空(if (instance == null)仍可能讀取到未更新的緩存值,導致不必要的鎖競爭。
  4. 第二次判空if (instance == null)t2可能會錯誤地認為instance == null,并再次執行實例化邏輯,導致又重復創建了新的實例。

內存可見性底層分析


  1. 硬件層面的原因
存儲層級讀寫速度存儲大小特性
寄存器最快最小(幾十字節)CPU直接計算使用的臨時存儲
CPU緩存 (L1/L2/L3)較小(KB~MB級)每個CPU核心/多核共享,減少訪問主存延遲
主內存 (RAM)大(GB級)所有線程共享,但訪問速度比緩存慢100倍以上
  • 速度差異:CPU為了避免等待慢速的主內存讀寫,會優先使用緩存和寄存器(如將instance的值緩存在核心的L1緩存中)。
  • 副作用:線程t1修改instance后,可能僅更新了當前核心的緩存,而其他核心的緩存或主內存未被同步,導致t2讀取到過期數據。

  1. Java內存模型(JMM)的抽象
  • 硬件差異被JMM抽象為 工作內存(線程私有)主內存(共享)的分離:
  • 工作內存:包含CPU寄存器、緩存等線程私有的臨時存儲
  • 主內存所有線程共享的真實內存

  1. 問題本質:
  • 當線程t1未強制同步(如缺少volatile或鎖)時,JVM/CPU可能延遲將工作內存的修改刷回主內存,其他線程也無法感知變更。

指令重排序

指令重排序的具體問題

img

instance = new SingletonLazy() 的實際操作可分為以下步驟(可能被JVM/CPU重排序):

1. 分配對象內存空間(堆上分配,此時內存內容為默認值0/null2. 調用構造函數(初始化對象字段)
3. 將引用賦值給 instance 變量(此時 instance != null

img
可能的危險重排序

  • JVM可能將步驟 3(賦值)2(構造) 調換順序,導致:
1. 分配內存
2. 賦值給 instance(此時 instance != null,但對象未初始化!)
3. 執行構造函數

img
這就是指令重排序問題。


  1. 多線程場景下指令重排序的后果
  • 線程 t1 執行 getInstance() 時發生重排序:
    • 先執行步驟1和3,instance 已不為 null,但對象未構造完成。
  • 線程 t2 調用 getInstance()
    • 第一次判空 if (instance == null) 會跳過
    • 若 t2 立刻調用 instance.func(),會訪問未初始化的字段,導致:img
      • 空指針異常(如果 func() 訪問未初始化的引用字段)。
      • 數據不一致(如果 func() 依賴構造函數中初始化的值)。

解決方法


使用volatile修飾instance后,不僅能確保每次讀取操作都直接從內存中讀取,還能防止與該變量相關的讀取和修改操作發生重排序。

private volatile static SingletonLazy instance;public static SingletonLazy getInstance() {if (instance == null) {          // 第一次無鎖檢查synchronized (locker) {      // 同步塊if (instance == null) {  // 第二次檢查instance = new SingletonLazy();  // 受volatile保護}}}return instance;
}

volatile 是怎么解決內存可見性問題的呢?


通過內存屏障(Memory Barrier)直接操作硬件層

  1. 寫操作:強制將當前核心的緩存行(Cache Line)寫回主內存,并失效其他核心的緩存。
  2. 讀操作:強制從主內存重新加載數據,跳過緩存。
private static volatile SingletonLazy instance; // 通過volatile禁止緩存優化

總結

  • 直接原因:CPU緩存和寄存器的速度優化導致可見性問題。
  • 根本原因:硬件架構與編程語言內存模型的設計差異(JMM需在性能與正確性間權衡)。
  • 解決方案volatile通過內存屏障強制同步硬件層和JMM的約定。

總結:為什么雙重檢查鎖(DCL)必須用volatile


  • 可見性:確保t1的初始化結果對t2立即可見。
  • 禁止指令重排序
    instance = new SingletonLazy() 的字節碼可能被重排序為:
    1. 分配內存空間
    2. 將引用寫入instance(此時instance != null但對象未初始化!)
    3. 執行構造函數
      volatile會禁止這種重排序,保證步驟2在3之后執行

4. 指令重排序問題


模擬編譯器指令重排序情景


要在超市中買到左邊購物清單的物品,有兩種買法

img


方法一:根據購物清單的順序買;(按照程序員編寫的代碼順序進行編譯)img

方法二:根據物品最近距離購買;(通過指令重排序后再編譯)

img

兩種方法都能買到購物清單的所有物品,但是比起第一種方法,第二種方法在不改變原有邏輯的情況下,優化執行指令順序,更高效地執行完所有的指令


指令重排序概述


指令重排序的定義

指令重排序是指編譯器或處理器為了提高性能,在不改變程序執行結果的前提下,對指令序列進行重新排序的優化技術。這種技術可以讓計算機在執行指令時更高效地利用計算資源,從而提高程序的執行效率。


指令重排序的類型

  1. 編譯器重排序

編譯器在生成目標代碼時會對源代碼中的指令進行優化和重排,以提高程序的執行效率。這一過程在編譯階段完成,目的是生成更高效的機器代碼。


  1. 處理器重排序

處理器在執行指令時也可以對指令進行重排序,以最大程度地利用處理器的流水線和多核等特性,從而提高指令的執行效率。


指令重排序引發的問題

盡管指令重排序可以提高程序的執行效率,但在多線程編程中可能會引發內存可見性問題。由于指令重排序可能導致共享變量的讀寫順序與代碼中的順序不一致,當多個線程同時訪問共享變量時,可能會出現數據不一致的情況。


指令重排序解決方案

為了解決指令重排序帶來的問題,可以采取以下措施:

  1. 編譯器層面:通過禁止特定類型的編譯器重排序,確保指令的執行順序符合預期。
  2. 處理器層面:通過插入內存屏障(Memory Barrier)來禁止特定類型的處理器重排序。內存屏障是一種CPU指令,用來禁止處理器指令發生重排序,從而保障指令執行的有序性。此外,內存屏障還會在處理器寫入或讀取值之前,將主內存的值寫入高速緩存并清空無效隊列,從而保障變量的可見性。

在這里插入圖片描述

在這里插入圖片描述

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

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

相關文章

2. 庫的操作

2.1 創建數據庫 語法: CREATE DATABASE [IF NOT EXISTS] db_name [create_specification [, create_specification] ...] create_specification: [DEFAULT] CHARACTER SET charset_name # 字符集: 存儲編碼 [DEFAULT] COLLATE collation_name # 校驗集: 比較/選擇/讀…

道可云人工智能每日資訊|北京農業人工智能與機器人研究院揭牌

道可云人工智能&元宇宙每日簡報(2025年6月3日)訊,今日人工智能&元宇宙新鮮事有: 北京農業人工智能與機器人研究院揭牌 5月30日,北京市農業農村局、北京市海淀區人民政府、北京市農林科學院共同主辦北京農業人…

【JSON-to-Video】設置背景視頻片斷

目錄 設置bgVideo字段 1. 設置bgVideo.videoList字段 2. 設置randomPlay字段 3. 設置complete字段 4. 調用API,制作視頻 歡迎來到JSON轉視頻系列教程。今天要教大家如何添加背景視頻片斷,在視頻制作中,巧妙運用背景視頻,能為…

星閃開發之Server-Client 指令交互控制紅燈亮滅案例解析(SLE_LED詳解)

系列文章目錄 星閃開發之Server-Client 指令交互控制紅燈亮滅的全流程解析(SLE_LED詳解) 文章目錄 系列文章目錄前言一、項目地址二、客戶端1.SLE_LED_Client\inc\SLE_LED_Client.h2.SLE_LED_Client\src\SLE_LED_Client.c頭文件與依賴管理宏定義與全局變…

Linux shell練習題

Shell 1. 判斷~/bigdata.txt 是否存在,若已存在則打印出”該文件已存在“,如不存在,則輸出打印:”該文件不存在“ if [ -f ./bigdata.txt ];then echo "文件存在" else echo "文件不存在" fi2. 判斷~/bigd…

Linux基本指令(三)

接上之前的文章,咱繼續分享Linux的基本指令,Linux指令比較多,很難全部記住需要做筆記對常用的指令進行記錄,方便以后復習查找,做筆記也可以對知識理解更加深刻。 目錄 時間相關指令 date顯示 時間戳 cal指令 ?編…

WebRTC中sdp多媒體會話協議報文詳細解讀

sdp介紹 在WebRTC(Web實時通信)中,SDP(Session Description Protocol)是用來描述和協商多媒體會話的協議。它定義了會話的參數和媒體流的信息,如音視頻編碼格式、傳輸方式、網絡地址等。SDP是WebRTC中一個…

【MySQL】 約束

一、約束的定義 MySQL 約束是用于限制表中數據的規則,確保數據的 準確性 和 一致性 。約束可以在創建表時定義,也可以在表創建后通過修改表結構添加。 二、常見的約束類型 2.1 NOT NULL 非空約束 加了非空約束的列不能為 NULL 值,如果可以…

【.net core】【watercloud】樹形組件combotree導入及調用

源碼下載:combotree: 基于layui及zTree的樹下拉框組件 鏈接中提供了組件的基本使用方法 框架修改內容 1.文件導入(路徑可更具自身情況自行設定) 解壓后將文件夾放在圖示路徑下,修改文件夾名稱為combotree 2.設置路徑(設置layu…

ES101系列07 | 分布式系統和分頁

本篇文章主要講解 ElasticSearch 中分布式系統的概念,包括節點、分片和并發控制等,同時還會提到分頁遍歷和深度遍歷問題的解決方案。 節點 節點是一個 ElasticSearch 示例 其本質就是一個 Java 進程一個機器上可以運行多個示例但生產環境推薦只運行一個…

CppCon 2015 學習:3D Face Tracking and Reconstruction using Modern C++

1. 3D面部追蹤和重建是什么? 3D面部追蹤(3D Face Tracking): 實時檢測并追蹤人臉在三維空間中的位置和姿態(如轉頭、點頭、表情變化等),通常基于攝像頭捕獲的視頻幀。3D面部重建(3D…

代碼中的問題及解決方法

目錄 YOLOX1. AttributeError: VOCDetection object has no attribute cache2. ValueError: operands could not be broadcast together with shapes (8,5) (0,)3. windows遠程查看服務器的tensorboard4. AttributeError: int object has no attribute numel YOLOX 1. Attribu…

【JVM】Java類加載機制

【JVM】Java類加載機制 什么是類加載? 在 Java 的世界里,每一個類或接口在經過編譯后,都會生成對應的 .class 字節碼文件。 所謂類加載機制,就是 JVM 將這些 .class 文件中的二進制數據加載到內存中,并對其進行校驗…

vue的監聽屬性watch的詳解

文章目錄 1. 監聽屬性 watch2. 常規用法3. 監聽對象和route變化4. 使用場景 1. 監聽屬性 watch watch 是一個對象,鍵是需要觀察的表達式,用于觀察 Vue 實例上的一個表達式或者一個函數計算結果的變化。回調函數的參數是新值和舊值。值也可以是方法名&am…

如何在 Ubuntu 24.04 服務器上安裝 Apache Solr

Apache Solr 是一個免費、開源的搜索平臺,廣泛應用于實時索引。其強大的可擴展性和容錯能力使其在高流量互聯網場景下表現優異。 Solr 基于 Java 開發,提供了分布式索引、復制、負載均衡及自動故障轉移和恢復等功能。 本教程將指導您如何在 Ubuntu 24.…

Linux內核中TCP三次握手的實現機制詳解

TCP三次握手是建立可靠網絡連接的核心過程,其在內核中的實現涉及復雜的協議棧協作。本文將深入分析Linux內核中三次握手的實現機制,涵蓋客戶端與服務端的分工、關鍵函數調用、協議號驗證及數據包處理流程。 一、三次握手的整體流程 三次握手分為三個階段,客戶端與服務端通過…

服務器--寶塔命令

一、寶塔面板安裝命令 ?? 必須使用 root 用戶 或 sudo 權限執行! sudo su - 1. CentOS 系統: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系統…

優化 Spring Boot API 性能:利用 GZIP 壓縮處理大型有效載荷

引言 在構建需要處理和傳輸大量數據的API服務時,響應時間是一個關鍵的性能指標。一個常見的場景是,即使后端邏輯和數據庫查詢已得到充分優化,當API端點返回大型數據集(例如,數千條記錄的列表)時&#xff0…

【WPF】WPF 項目實戰:構建一個可增刪、排序的光源類型管理界面(含源碼)

💡WPF 項目實戰:構建一個可增刪、排序的光源類型管理界面(含源碼) 在實際的圖像處理項目中,我們經常需要對“光源類型”進行篩選或管理。今天我們來一步步構建一個實用的 WPF 界面,實現以下功能&#xff1…

C++23 已棄用特性

文章目錄 1. std::aligned_storage 與 std::aligned_union1.1 特性介紹1.2 被棄用的原因1.3 替代方案 2. std::numeric_limits::has_denorm2.1 特性介紹2.2 被棄用的原因 3. 總結 C23 已棄用特性包括:std::aligned_storage、std::aligned_union 與 std::numeric_lim…