設計模式之單例模式及其在多線程下的使用

很多時候,我們在使用類創建類的實例并不想可以創建很多實例對象,比如在數據庫連接的時候,對于一個數據庫的連接通常只需要連接池中的某個連接的實例,連接一次即可,對于session會話,用戶在訪問網頁做會話保持的時候,一個用戶只需要一個實例來表示本次會話即可。

設計模式就像是下圍棋那樣的一些定式,棋譜,如果對方小飛掛角,我們可以選擇小飛守角,或者大飛守角等等,也就說如果一個棋力不足的新手能把一部分常用的定式或者棋譜背下來,那么遇到了類似的情況或者定式的招數就可以運用出來,而不至于下的一塌糊涂,也就是為新手菜狗提供了保底的手法。

1.單例模式

在23種典型的設計模式中就存在一種單例模式,可以很好的解決我們最初提到的,"全局單個實例"的場景,就是"單例模式"很有見名知意的感覺。

1.1 定義

單例模式在大佬給出的定義是:

通常我們可以讓一個全局變量使得一個對象被訪問,但它不能防止你實例化多個對象。一個最好的辦法就是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例可以被創建,并且它可以提供一個訪問該實例的辦法。

1.提供全局變量使得一個對象被訪問是什么意思呢?

MyClass instance = new MyClass(); // 每次都可以創建新的實例

可以把instance設置為一個全局變量,比如

private static final MyClass instance = new MyClass();

?2.讓類自身負責保存它唯一的實例是什么意思?

這個類做到兩件事:

  1. 私有化構造方法,別人就不能隨便 new 它了;

  2. 在類中自己創建并保存唯一的實例

  3. 提供一個公開的靜態方法用于獲取該實例

1.2 單例模式的使用

在單例模式這個定式中,存在兩種手法

1.餓漢式的單例模式

餓漢式就是,在提供全局變量的時候,就為這個全局變量創建一個實例,這個全局變量和實例一般設置為static,這樣他就會隨著類的加載就進行初始化。

public class Singleton {// 1. 提前創建好唯一實例(類加載時就實例化)private static final Singleton instance = new Singleton();// 2. 構造方法私有化,防止外部 newprivate Singleton() {}// 3. 提供靜態方法讓外部訪問實例public static Singleton getInstance() {return instance;}
}
2.懶漢式的單例模式

懶漢式就是,在提供全局變量的時候并不為這個全局變量創建實例,而是等到使用時在提供的全局訪問點,也就是提供的靜態方法去獲得實例的時候,在進行創建實例,這樣就減少了類加載時的一些初始化工作。

public class Singleton {private static Singleton instance;private Singleton() {}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton(); // 延遲加載}return instance;}
}

單例模式除了可以保證唯一的實例外,還有什么好處呢?

比如單例模式因為Singleton類封裝他的唯一實例,這樣它可以嚴格控制客戶怎么樣訪問它以及合適訪問它,簡單來說就是對唯一實例的受控訪問。

單例模式通過自己管理自己的唯一實例(比如通過 private static Singleton instance),并只提供一個公開的獲取方法(如 getInstance(),從而實現對這個實例的訪問控制

  • 外部不能隨便 new 一個新的對象;

  • 外部必須通過你提供的方式來訪問;

  • 類本身可以在需要的時候控制創建時機(比如懶漢式延遲創建);

這就叫“對唯一實例的受控訪問”。

單例模式看起來有點像實用類中的靜態方法,比如Math類有很多數學計算方法,他們之間雖然很類似,實用類通常也會采用私有化的構造方法來避免其有實例。但是他們還是有很多不同的

單例類和工具類(實用類)在結構上是有點像的,比如:

  • 都私有了構造方法,不允許 new

  • 都通過類名來訪問功能(方法或實例);

比如 Math.abs(-1) 這樣的調用方式也不用創建對象,看起來就和 Singleton.getInstance() 類似。

1.實用類不保存狀態,僅提供一些靜態方法或者靜態屬性來讓我們使用,單例模式卻是有狀態的。

2.實用類不能用于繼承多態,而單例模式雖然實例唯一,卻可以有子類來繼承

3.實用類只不過是一些方法屬性的集合,而單例模式確實有著唯一的對象實例。

2.多線程下的單例模式

很多代碼程序在單線程下運行的十分完美,但是到了多線程的環境下就會暴露出很多短板甚至是bug,比如上面的單例模式,在多個線程同時,注意是同時訪問Singleton類,調用getInstance方法是會有可能創建多個實例的。

很尷尬,那應該怎么解決呢?

線程安全問題的發現與解決-CSDN博客

我們在前面分析了,多線程下的線程安全問題,這種情況就屬于線程安全的問題之一,

是因為,修改操作不是原子的情況所造成的

比如下面的代碼

/*** 懶漢式單例模式*/
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy(){};public static SingletonLazy getInstance(){if(instance == null){instance = new SingletonLazy();}return instance;}
}

我們發現,在getInstance中并沒有像之前提到的count++這樣的修改操作呀

也就是有個?

if(instance == null)//判斷
instance = new SingletonLazy();//賦值
return instance;

返回操作是一種"讀操作"通常不會是多線程下bug的元兇

那么原因是因為if(instance == null)//判斷 或者 instance = new SingletonLazy();//賦值 再或者是二者合并起來造成的問題嗎?

Java中的賦值操作,確實本質上是一種"讀操作"也不應該是造成問題的原因,

原因是第三種情況,拆開各自安好,合并就可能會出現問題了,因為if(instance == null)//判斷 和instance = new SingletonLazy();//賦值 二者放在一起是一個完整的邏輯。

1.多線程改進1

?問題核心就是線程不安全導致重復實例化

我們不妨嘗試一下加鎖,讓不是原子性的操作變成加鎖后的原子性的操作

synchronized (SingletonLazy.class){if(instance == null){instance = new SingletonLazy();}}

之前我們討論解決線程安全時講過synchronized的使用

在此處的getInstance方法中,想要加鎖因為是靜態方法的緣故,就要使用當前類的Class對象來充當鎖對象。

如果想要使用實例的鎖對象也是可以的可以這樣寫代碼:

/*** 懶漢式單例模式*/
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy(){};private static final Object lock = new Object();public static SingletonLazy getInstance(){synchronized (lock){if(instance == null){instance = new SingletonLazy();}}return instance;}
}

關于鎖的問題,我們不再討論,現在我們來看一下加鎖后的效果

  1. 線程1 搶到了鎖,成功進入 synchronized (lock) 的同步塊;

  2. 線程1 執行判斷:instance == null,結果為 true

  3. 線程1 開始創建單例對象(執行 new SingletonLazyO());

  4. 此時線程2 也調用了 getInstance(),但因為同步塊已經被線程1占用,所以線程2在 synchronized 外面等待

  5. 線程1 創建完實例后,退出同步塊,釋放了鎖,并且 instance 已經指向新建好的對象;

  6. 線程2 被喚醒,獲取到了鎖,進入同步塊;

  7. 再次檢查 instance == null,這次結果為 false(因為線程1已經創建好了);

  8. 線程2 直接返回現有的實例,避免了重復創建

  • 最終,兩個線程都獲得了同一個對象實例;

  • 沒有出現重復創建或資源浪費的問題;

  • 符合單例模式“全局唯一實例”的設計目標;

  • 這種方式雖然線程安全,但每次訪問都進入同步塊,性能稍差,可以通過雙重檢查優化

2.多線程改進2

我們知道加鎖是存在一定的代價的

為了避免每次都進入 synchronized 塊,可以使用“雙重檢查鎖”:?

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

初次見這種雙重if而且內外if條件還是相同的,很多新手會覺得代碼邏輯很混亂

if (instance == null) — 第一次檢查(不加鎖

  • 這是性能優化的關鍵:

    • 大多數時候,instance 已經被創建了,不需要進入同步代碼塊;

    • 只有第一次創建的時候才需要同步;

    • 避免每次都加鎖,提高效率


synchronized (Singleton.class)

  • 加鎖的對象是類的 .class 對象,因為 instance 是靜態變量,是整個類共享的;

  • 保證只有一個線程可以創建實例

  • 是解決線程安全的核心。


if (instance == null) — 第二次檢查(加鎖后再確認

  • 為什么要檢查兩次?

    • 如果不再判斷一遍,多個線程可能都在排隊等鎖;

    • 第一個線程創建了對象,釋放鎖后,后面的線程仍然會再創建一次,如果不檢查;

    • 所以要加鎖后再檢查一次,防止重復創建。


instance = new Singleton();

  • 真正創建對象的地方;

  • 只有在加鎖的前提下,并且確認 instance 為 null 的時候才會執行。

3.多線程改進3

上面的代碼仍然存在一定的缺陷,我們還有一種很隱匿的缺陷沒有找到,那就是指令重排序的問題

線程安全問題的發現與解決-CSDN博客

前面我們提到,線程安全的幾大問題其中之一就是,修改操作不是原子的

new SingletonLazy()

這條語句看起來僅僅只是Java的一條普通的實例化語句,但是在JVM層面就包括了三個步驟

1.為該實例開辟內存空間,分配內存

2.初始化該實例對象

3.最后instance引用賦值,引用這一塊內存空間

編譯器會覺得,如果我快點引用,先不初始化能不能讓代碼執行的更快,更高效呢?

所以它大膽的調換了執行順序變成了

1.為該實例開辟內存空間,分配內存

3.instance引用賦值,引用這一塊內存空間

2.初始化該實例對象

這不換不要緊,一換的話,如果存在別的線程在“賦值”和“初始化之間”訪問這個對象,順便修改了就會造成bug。

假如線程 A 執行到 instance = new SingletonLazy();,由于重排序:

  • 它已經把 instance 指向了還“沒初始化”的對象

  • 此時線程 B 也進來了,看到 instance != null,以為已經初始化好了

  • 然后就直接拿這個對象用了(return instance)!結果呢?對象狀態是不完整的!

這就產生了嚴重的**“半初始化對象被訪問”**的問題

這里其實也說明了為什么單線程下指令重排序根本沒有問題

因為

不存在別的線程在“賦值”和“初始化之間”訪問這個對象,也就不存在bug。

3.多線程下單例模式的使用總結

綜上,很多代碼在單線程下生龍活虎,因為單線程下沒有其他線程來“搶時間”、“搶資源”,所以很多細節(比如原子性、可見性、重排序)根本不會暴露出來。這就是為什么并發編程下會出現很多的問題,所以我們在使用單例模式的多線程版本的時候,要記得以下兩點

  • 使用雙重 if 判定(Double-Checked Locking)
    避免每次獲取實例都加鎖,提高性能。

  • 在實例變量上添加 volatile 關鍵字
    防止 JVM 發生指令重排序,確保對象初始化的完整性。

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

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

相關文章

Apache Ignite 2.8 引入的新指標系統(New Metrics System)的完整說明

這段文檔是關于 Apache Ignite 2.8 引入的“新指標系統(New Metrics System)” 的完整說明。這是 Ignite 監控體系的一次重大升級,相比舊的、分散的統計方式,新系統更統一、靈活、可擴展。 我們來逐層拆解、通俗易懂地理解這個新…

【氮化鎵】GaN同質外延p-i-n二極管中星形與三角形擴展表面缺陷的電子特性

2025年7月23日,美國國家標準與技術研究院(NIST)與美國海軍研究實驗室的Andrew J. Winchester等人在《Applied Physics Letters》期刊發表了題為《Electronic properties of extended surface defects in homoepitaxial GaN diodes》的文章,基于光電發射電子顯微術、導電原子…

使用 Scrapy 框架定制爬蟲中間件接入淘寶 API 采集商品數據

一、引言 在電商數據分析、市場調研等領域,獲取淘寶平臺上的商品數據是一項常見需求。淘寶提供了 API 接口,允許開發者通過授權的方式獲取商品信息。本文將介紹如何使用 Scrapy 框架定制爬蟲中間件,實現對淘寶 API 的接入,從而高…

Jmeter全局變量跨線程組的使用

一、線程組1中從數據庫中查詢到字段值二、BeanShell取樣器中設置為全局變量#為什么說props.put("Out1",Out);其實是設置Out1為Jmeter的屬性了呢? 因為在后面的調試取樣器運行結果中,會發現如果只打開顯示變量開關,是看不到Out1運行…

前端技術棧詳解

前端技術棧是指構建現代Web應用程序所需的一系列技術和工具的集合。以下是當前主流前端技術棧的詳細解析&#xff1a; 一、核心基礎技術 1. HTML5 作用&#xff1a;網頁內容的結構化標記關鍵特性&#xff1a; 語義化標簽&#xff08;<header>, <section>, <arti…

Git Pull 時遇到 Apply 和 Abort 選項?詳解它們的含義與應對策略

在使用 Git 進行團隊協作時&#xff0c;git pull 是最常用的命令之一&#xff0c;用于拉取遠程倉庫的最新代碼并合并到本地分支。但有時執行 git pull 后&#xff0c;Git 會提示 ?Apply&#xff08;應用&#xff09;?? 和 ?Abort&#xff08;中止&#xff09;?? 兩個選項…

暑期算法訓練.11

目錄 47. 力扣203 移除鏈表元素 47.1 題目解析&#xff1a; ?編輯 47.2 算法思路&#xff1a; 47.3 代碼演示&#xff1a; ?編輯 48. 力扣2.兩數相加 48.1 題目解析&#xff1a; ?編輯 48.2 算法思路; 48.3 代碼演示&#xff1a; 48.4 總結反思&#xff1a; …

nl2sql grpo強化學習訓練,加大數據量和輪數后,準確率沒提升,反而下降了,如何調整

在NL2SQL任務中使用GRPO強化學習訓練時&#xff0c;增加數據量和訓練輪數后準確率下降&#xff0c;通常是由過擬合、訓練不穩定、獎勵函數設計不合理、數據質量問題或探索-利用失衡等原因導致的。以下是具體的診斷思路和調整策略&#xff0c;幫助定位問題并優化性能&#xff1a…

PHP/Java/Python實現:如何有效防止惡意文件上傳

文章目錄 木馬病毒防范:文件上傳如何徹底防止偽造文件類型 引言 一、文件類型偽造的原理與危害 1.1 常見偽造手段 1.2 潛在危害 二、防御體系設計 2.1 防御架構 三、核心防御技術實現 3.1 服務端驗證實現 3.1.1 文件內容檢測(Python示例) 3.1.2 擴展名與內容雙重驗證(Java示…

SpringBoot系列之基于Redis的分布式限流器

SpringBoot系列之基于Redis的分布式限流器 SpringBoot 系列之基于 Redis 的分布式限流器 圖文并茂,代碼即拷即用,支持 4 種算法(固定窗口 / 滑動窗口 / 令牌桶 / 漏桶) 一、為什么要用分布式限流? 單機 Guava-RateLimiter 在集群下會 各玩各的,流量漂移,無法全局控量。…

面試遇到的問題2

Redisson的看門狗相關問題 首先要明確一點&#xff0c;看門狗機制的使用方式是&#xff1a;在加鎖的時候不加任何參數&#xff0c;也就是&#xff1a; RLock lock redisson.getLock("myLock"); try {lock.lock(); // 阻塞式加鎖// 業務邏輯... } finally {lock.unl…

Linux—進程概念與理解

目錄 1.馮諾依曼體系結構 小結&#xff1a; 2.操作系統 概念&#xff1a; 結構示意圖&#xff1a; 理解操作系統&#xff1a; 用戶使用底層硬件層次圖&#xff1a;?編輯 3.進程 概念 結構示意圖 task_ struct內容分類 典型用法示例 觀察進程: 了解 PID PPID 查…

LeetCode 面試經典 150_數組/字符串_買賣股票的最佳時機(7_121_C++_簡單)(貪心)

LeetCode 面試經典 150_數組/字符串_買賣股票的最佳時機&#xff08;7_121_C_簡單&#xff09;題目描述&#xff1a;輸入輸出樣例&#xff1a;題解&#xff1a;解題思路&#xff1a;思路一&#xff08;貪心算法&#xff09;&#xff1a;代碼實現代碼實現&#xff08;思路一&…

Ubuntu 18.04 repo sync報錯:line 0: Bad configuration option: setenv

repo sync時報 line 0: Bad configuration option: setenv因為 Ubuntu 18.04 默認的 openssh-client 是 7.6p1&#xff0c;還不支持 setenv&#xff0c;但是.repo/repo/ssh.py 腳本中明確地傳入了 SetEnv 參數給 ssh&#xff0c;而你的 OpenSSH 7.6 不支持這個參數。需要按如下…

bug記錄-stylelint

BUG1不支持Vue文件內聯style樣式解決&#xff1a; "no-invalid-position-declaration": null

前端開發(HTML,CSS,VUE,JS)從入門到精通!第一天(HTML5)

一、HTML5 簡介1&#xff0e;HTML全稱是 Hyber Text Markup Language&#xff0c;超文本標記語言&#xff0c;它是互聯網上應用最廣泛的標記語言&#xff0c;簡單說&#xff0c;HTML 頁面就等于“普通文本HTML標記&#xff08;HTML標簽&#xff09;“。2&#xff0e;HTML 總共經…

智慧收銀系統開發進銷存:便利店、水果店、建材與家居行業的—仙盟創夢IDE

在數字化轉型的浪潮中&#xff0c;收銀系統已不再局限于簡單的收款功能&#xff0c;而是成為企業進銷存管理的核心樞紐。從便利店的快消品管理到建材家居行業的大宗商品調度&#xff0c;現代收銀系統通過智能化技術重塑了傳統商業模式。本文將深入探討收銀系統在不同行業進銷存…

三維掃描相機:工業自動化的智慧之眼——遷移科技賦能智能制造新紀元

在當今工業4.0時代&#xff0c;自動化技術正重塑生產流程&#xff0c;而核心工具如三維掃描相機已成為關鍵驅動力。作為工業自動化領域的“智慧之眼”&#xff0c;三維掃描相機通過高精度三維重建能力&#xff0c;解決了傳統制造中的效率瓶頸和精度痛點。遷移科技&#xff0c;自…

Jmeter的元件使用介紹:(九)監聽器詳解

監聽器主要是用來監聽腳本執行的取樣器結果。Jmeter的默認監聽器有&#xff1a;查看結果樹、聚合報告、匯總報告、用表格查看結果&#xff0c;斷言結果、圖形結果、Beanshell監聽器、JSR223監聽器、比較斷言可視化器、后端監聽器、郵件觀察器&#xff0c;本文介紹最常用的監聽器…

聯通元景萬悟 開源,搶先體驗!!!

簡介&#xff1a; 元景萬悟智能體平臺是一款面向企業級場景的一站式、商用license友好的智能體開發平臺&#xff0c;是業界第一款go語言&#xff08;后端&#xff09;開發的智能體開發平臺&#xff08;7月19日&#xff09;&#xff0c;coze studio開源是7月26日&#xff0c;同時…