多線程代碼案例-1 單例模式

單例模式

單例模式是開發中常見的設計模式。

設計模式,是我們在編寫代碼時候的一種軟性的規定,也就是說,我們遵守了設計模式,代碼的下限就有了一定的保證。設計模式有很多種,在不同的語言中,也有不同的設計模式,設計模式也可以被認為是對編程語言語法的補充

單例即單個實例(對象),某個類在一個進程中,只應該創建出一個實例(原則上不應該創建出多個實例),使用單例模式,可以對我們的代碼進行一個更為嚴格的校驗和檢查。

舉個例子:有時候,代碼中需要管理/持有大量的數據,此時有一個對象就可以了。比如:我需要一個對象管理10G的數據,如果我們不小心創建出多個對象,內存空間就會成倍地增長。

如何保證只有唯一的對象呢?我們可以選擇“君子之約地方式”,寫一個文檔,文檔上約定,每個接手維護代碼的程序員,都不能對這個類創建多個實例(很顯然,這種約定并不靠譜)我們期望讓機器(編譯器)能夠對代碼中的指定類,對創建的實例個數進行檢驗。如果發現創建出了多個實例,就直接編譯報錯,但是Java語法中本身沒有辦法直接約定某個對象能創建出幾個實例,那么就需要程序員使用一些技巧來實現這樣的效果。

實現單例模式的方式有很多種,這里介紹兩種實現方式:餓漢模式和懶漢模式。

1 餓漢模式

代碼如下:

//餓漢模式
//期望這個類只能有唯一的實例(一個進程中)
class Singleton{private static Singleton instance = new Singleton();//在這個類被加載時,就會初始化這個靜態成員,實例創建的時機非常早——餓漢public static Singleton getInstance(){//其他代碼想要使用這個類的實例就需要通過這個方法進行獲取,// 不應該在其他代碼中重新new這個對象而是使用這個方法獲取這個現有的對象return instance;}private Singleton(){//其他代碼就沒法new了}
}

在這個類中,我們創建出了唯一的對象,被static修飾,說明這個變量是類變量,(由類對象所擁有(每個類的類對象只存在一個),在類加載的時候,它就已經被初始化了)

而將構造方法設為私有,就使得只能在當前類里面創建對象了,其他位置就不能再創建對象了,因此這個instance指向的對象就是唯一的對象。

其他代碼要想使用這個類的實例,就需要通過這個getInstance()方法獲取這個對象,而無法在其他代碼中new一個對象。

上述代碼,稱為”餓漢模式“,是單例模式中的一種簡單的寫法,”餓“形容”非常迫切“,實例在類加載的時候就創建了,創建的時機非常早,相當于程序一啟動,實例就創建了。?

但是,上面的代碼,面對反射,是無能為力的,也就是說,仍然可以通過反射來創建對象,但反射是屬于非常規的編程手段,代碼中隨意使用反射是非常糟糕的。

2 懶漢模式

”懶“這個詞,并不是貶義詞,而是褒義詞。社會能進步,科技能發展,生產效率提高,有很大部分原因都是因為懶。

舉個生活中的例子(不考慮衛生):

假如我每次吃完飯就洗碗,那我每次就需要洗全部的碗;但是如果我每次吃完飯把碗放著,等到下次吃飯的時候再洗,此時,如果我只要用到兩個碗,那我就只需要洗兩個碗就行了,很明顯洗兩個碗要比洗全部碗更加高效。

在計算機中,”懶“的思想就非常有意思,它通常代表著更加高效

比如有一個非常大的文件(10GB),使用編輯器打開這個文件,如果是按照”餓漢“的方式 ,編輯器就會先把這10GB的數據都加載到內存中,然后再進行統一的展示。(但是加載了這么多數據,用戶還是需要一點一點地看,沒法一下子看完這么多)

如果是按照”懶漢“地方式,編輯器就會只讀取一小部分數據(比如只讀取10KB),把這10KB先展示出來,然后隨著用戶進行翻頁之類的操作,再繼續展示后面的數據。

加載10GB的時間會很長,但是加載10KB卻只是一瞬間的事情……

懶漢模式,區別于餓漢模式,創建實例的時機不一樣了,創建實例的時機會更晚,一直到第一次使用getInstance方法時才會創建實例。

代碼如下(注意:這是一個不完整的代碼,因為還有一些線程安全問題需要解決~~):

//懶漢的方式實現單例模式class SingletonLazy{private static SingletonLazy instance = null;public static  SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}}}//不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}

第一行代碼中仍然是先創建一個引用,但是這個引用不指向任何的對象。如果是首次調用getInstance方法,就會進入if條件,創建出對象并且讓當前引用指向該對象。如果是后續調用getInstance方法,由于當前的instance已經不是null了,就會返回我們之前創建的引用了。

這樣設定,仍然可以保證,該類的實例是唯一一個,與此同時,創建實例的時機就不再是程序驅動了,而是當第一次調用getInstance的時候,才會創建。。

而第一次調用getInstance這個操作的執行時機就不確定了,要看程序的實際需求,大概率會比餓漢這種方式要晚一些,甚至有可能整個程序壓根用不到這個方法,也就把創建的操作給省下了。

有的程序,可能是根據一定的條件,來決定是否要進行某個操作,進一步來決定是否要創建實例。?

3 單例模式與線程安全

上面我們介紹的關于單例模式只是一個開始,接下來才是我們多線程的真正關鍵問題。即:上述我們編寫的餓漢模式和懶漢模式,是否是線程安全的?

餓漢模式:

//餓漢模式
//期望這個類只能有唯一的實例(一個進程中)
class Singleton{private static Singleton instance = new Singleton();//在這個類被加載時,就會初始化這個靜態成員,實例創建的時機非常早——餓漢public static Singleton getInstance(){//其他代碼想要使用這個類的實例就需要通過這個方法進行獲取,// 不應該在其他代碼中重新new這個對象而是使用這個方法獲取這個現有的對象return instance;}private Singleton(){//其他代碼就沒法new了}
}

對于餓漢模式來說,getInstance直接返回instance這個實例,這個操作,本質上就是一個的操作(多個線程同時讀取同一變量,是不會產生線程安全問題的)。因此,在多線程下,它是線程安全的。

懶漢模式 :

//懶漢的方式實現單例模式class SingletonLazy{private static SingletonLazy instance = null;public static  SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}//不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}

再看懶漢模式,在懶漢模式中,代碼中有的操作(return instance),又有的操作(instance = new SingletonLazy())。?很明顯,這是一個有線程安全問題的代碼!!!

問題1:線程安全問題

因為多線程之間是隨機調度,搶占是執行的,如果t1和 t2 按照下列的順序執行代碼,就會出現問題。

如果是t1和t2按照上述情況操作,就會導致實例被new了兩次,這就不是單例模式了,就會出現bug了!!!

那如何解決當前的代碼bug,使它變為一個線程安全的代碼呢?

加鎖~~

知道要加鎖了?那大家不妨想想:如果我把鎖像如下代碼這樣加下去,是否線程就安全了呢?

class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static  SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了if (instance == null) {//如果首次調用就創建實例sychronized(locker){instance = new SingletonLazy();}}//不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}

答案很顯然:不行!!!因為如上述代碼加鎖仍然會發生剛才那樣的線程不安全的情況。

所以這里如果想要代碼正確執行,需要把if和new兩個操作,打包成一個原子的操作(即加鎖加在if語句的外面)。?

class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static  SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了synchronized(locker){    if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}}  //不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}

?

此時因為t1拿到了鎖,t2進入阻塞,等t1執行完畢后(創建完對象后),t2進行判斷,此時因為t1已經創建好了對象,所以t2就只能返回當前對象的引用了。?

多線程的代碼是非常復雜的,代碼稍微變化一點,結論就可能截然不同。千萬不能認為,代碼中加了鎖就一定線程安全,不加鎖就一定線程不安全,具體問題要具體分析,要分析這個代碼在各種調度執行順序下不同的情況,確保每種情況都不會出現bug!!!

?問題2:效率問題

上述代碼還存在的另一個問題是效率問題:試想一下,當你創建完這個單例對象,你每次獲取這個單例對象時(是讀的操作,并不會有線程問題),每次都要去加鎖、解鎖,然后才能返回這個對象。(注意:加鎖、解鎖耗費的空間和時間都是很大的)。

所以為了優化上面的代碼,我們可以再加上一層if,如果instance為null(需要執行寫操作),考慮到線程安全問題,就需要加鎖;如果instance不為null了,就不需要加鎖了。

class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static  SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了if(instance == null){synchronized(locker){    if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}}}    //不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}

上面的代碼,有兩重完全相同if判斷條件,但是他們的作用是完全不同的:

第一個if是判斷是否需要加鎖,第二個if是判斷是否要創建對象!!!

巧合的是,兩個if條件相同,但是他們的作用是完全不同的,這樣就實現了雙重校驗鎖。在以后的學習中,還可能出現兩個if條件是相反的情況。

問題3:指令重排序問題

這個代碼還有一點問題需要解決:我們之前在線程安全的原因中講過的:指令重排序問題就在懶漢模式上出現了~~

指令重排序,也是編譯器優化的一種方式。編譯器會在保證邏輯不變的前提下,為了提高程序的效率,調整原有代碼的執行順序。

再舉個生活中的例子:

我媽讓我去超市買東西:西紅柿、雞蛋、黃瓜、茄子。

超市攤位分布圖如下:

如果我按我媽給的順序,那就會走出這樣的路線:?

上述方案雖然也能完成我媽給的任務,但如果我對超市已經足夠熟悉了,我就能夠在保證邏輯不變

的情況下(買到4種菜),調整原有買菜的執行順序,提高買菜效率:?

返回到代碼中:

   instance = new SingletonLazy();

?上面這行代碼,可以拆分為三個步驟:

1、申請一段內存空間。

2、調用構造方法,創建出當前實例。

3、把這個內存地址賦給instance這個引用。

上述代碼可以按1、2、3這個順序來執行,但是編譯器也可能會優化成1、3、2這個順序執行。這兩種順序在單線程下都是能夠完成任務的。

1就相當于買了個房子

2相當于裝修房子

3相當于拿到了房子的鑰匙

通過1、2、3得到的房子,拿到的房子已經是裝修好的,稱為“精裝房”;通過1、3、2得到的房子,拿到的房子需要自己裝修,稱為“毛坯房”,我們買房子時,上面的兩種情況都可能發生。

但是,如果在多線程環境下,指令重排序就會引入新問題了。

上述代碼中,由于 t1 線程執行完 1 3 步驟(申請一段內存空間,把內存空間的地址賦給引用變量,但并沒有進行 2 調用構造方法的操作,會導致 instance指向的是一個未被初始化的對象)之后調度走,此時 instance 指向的是一個非 null 的,但是是未初始化的對象,此時 t2 線程判定 instance == null 不成立,就會直接 return,如果 t2 繼續使用 instance 里面的屬性或者方法,就會出現問題,引起代碼的邏輯出現問題。?

那么我們應該如何解決當前問題呢?

volatile關鍵字

之前講過volatile有兩個功能:

1、保證內存可見性:每次訪問變量都必須要重新讀取內存,而不會優化為讀寄存器/緩存。

2、禁止指令重排序:針對被volatile修飾的變量的讀寫操作的相關指令,是不能被重排序的。

懶漢模式的完整代碼:

//經典面試題!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
package Thread;
//懶漢的方式實現單例模式
//線程不安全,它在多線程環境下可能會創建多個實例
class SingletonLazy{//這個引用指向唯一實例,這個引用先初始化為null,而不是立即創建實例
private volatile static SingletonLazy instance = null;//針對這個變量的讀寫操作就不能重排序了
private static Object locker;
//第一次if判定是否要加鎖,第二次if判定是否要創建對象//雙重校驗鎖public static  SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了//加鎖效率不高,且容易導致阻塞,所以再加一個判斷提高效率if(instance ==null) {//判斷是否為空,為空再加鎖//不為空,說明是后續的調用就無需加鎖了synchronized (locker) {if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}}}//不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}

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

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

相關文章

【計算機組成原理】第二部分 存儲器--分類、層次結構

文章目錄 分類&層次結構0x01 分類按存儲介質分類按存取方式分類按在計算機中的作用分類 0x02 層次結構 分類&層次結構 0x01 分類 按存儲介質分類 半導體存儲器磁表面存儲器磁芯存儲器光盤存儲器 按存取方式分類 存取時間與物理地址無關(隨機訪問&#…

迅為RK3588開發板安卓GPIO調用APP運行測試

將網盤上的安卓工程文件復制到 Windows 電腦上。確保工程路徑中使用英文字符,不包含中文。接著,啟動 Android Studio,點擊“Open”按鈕選擇應用工程文件夾,然后點擊“OK”。由于下載 Gradle 和各種 Jar 包可能需要一段時間&#x…

BFS算法篇——打開智慧之門,BFS算法在拓撲排序中的詩意探索(下)

文章目錄 引言一、課程表1.1 題目鏈接:https://leetcode.cn/problems/course-schedule/description/1.2 題目分析:1.3 思路講解:1.4 代碼實現: 二、課程表||2.1 題目鏈接:https://leetcode.cn/problems/course-schedul…

計數循環java

import java.util.Scanner;public class Hello {public static void main(String[] args) {Scanner in new Scanner(System.in);int count 10;while(count > 0) {count count -1;System.out.println(count);}System.out.println(count);System.out.println("發射&am…

11. CSS從基礎樣式到盒模型與形狀繪制

在前端開發中,CSS(層疊樣式表)是控制網頁樣式和布局的核心技術。整理了關于 CSS 基礎樣式、文本樣式、盒模型以及形狀繪制的一些心得。以下是詳細的學習筆記。 一、基礎樣式設置 1. 字體樣式 字體樣式是網頁視覺呈現的重要組成部分&#xf…

雙種群進化算法:動態約束處理與資源分配解決約束多目標優化問題

雙種群進化算法:動態約束處理與資源分配解決約束多目標優化問題 一、引言 約束多目標優化問題(CMOPs)在工程設計、資源分配等領域廣泛存在,其核心是在滿足多個約束條件的同時優化多個目標函數。傳統方法往往難以平衡約束滿足與目…

【Qt】pro工程文件轉CMakeLists文件

1、簡述 Qt6以后默認使用cmake來管理工程,之前已經一直習慣使用pro,pro的語法確實很簡單、方便。 很多項目都是cmake來管理,將它們加入到Qt項目中,cmake確實是大勢所趨。比如,最近將要開發的ROS項目,也是使用的cmake語法。 以前總結的一些Qt代碼,已經編寫成pro、pri等…

手機換地方ip地址會變化嗎?深入解析

在移動互聯網時代,我們經常帶著手機穿梭于不同地點,無論是出差旅行還是日常通勤。許多用戶都好奇:當手機更換使用地點時,IP地址會隨之改變嗎?本文將深入解析手機IP地址的變化機制,幫助您全面了解這一常見但…

【Canda】常用命令+虛擬環境創建到選擇

目錄 一、conda常用命令 二、conda 環境 2.1 創建虛擬環境 2.2 conda環境切換 2.3 查看conda環境 2.4 刪除某個conda環境 2.5 克隆環境 三、依賴包管理 3.1 安裝命令 3.2 更新包 3.3 卸載包 3.4 查看環境中所有包 3.5 查看某個包的版本信息 3.6 搜索包 四、環境…

目標檢測任務常用腳本1——將YOLO格式的數據集轉換成VOC格式的數據集

在目標檢測任務中,不同框架使用的標注格式各不相同。常見的框架中,YOLO 使用 .txt 文件進行標注,而 PASCAL VOC 則使用 .xml 文件。如果你需要將一個 YOLO 格式的數據集轉換為 VOC 格式以便適配其他模型,本文提供了一個結構清晰、…

Python作業練習2

任務簡述 if_name__main_的含義,why? 問題解答 在Python中,if __name__ __main__:是一種常見的慣用法,用于檢查當前模塊是否是主程序入口點。要理解其含義和用途,首先需要了解兩個概念: 1. __name__: 這是一個特…

ppy/osu構建

下載 .NET (Linux、macOS 和 Windows) | .NET dotnet還行 構建:f5 運行:dotnet run --project osu.Desktop -c Debug

NY182NY183美光固態顆粒NY186NY188

NY182NY183美光固態顆粒NY186NY188 在存儲技術的競技場上,美光科技(Micron)始終扮演著革新者的角色。其NY系列固態顆粒憑借前沿的3D NAND架構和精準的工藝控制,成為企業級存儲和數據中心的關鍵支柱。本文將圍繞NY182、NY183、NY1…

C++的歷史與發展

目錄 一、C 的誕生與早期發展 (一)C 語言的興起與局限 (二)C 的雛形:C with Classes (三)C 命名與早期特性豐富 二、C 的主要發展歷程 (一)1985 年:經典…

DedeCMS-Develop-5.8.1.13-referer命令注入研究分析 CVE-2024-0002

本次文章給大家帶來代碼審計漏洞挖掘的思路,從已知可控變量出發或從函數功能可能照成的隱患出發,追蹤參數調用及過濾。最終完成代碼的隱患漏洞利用過程。 代碼審計挖掘思路 首先flink.php文件的代碼執行邏輯,可以使用php的調試功能輔助審計 …

計算機網絡|| 常用網絡命令的作用及工作原理

1.hostname 作用:顯示計算機的完整計算機名的主機名部分。僅當 Internet 協議 (TCP/IP) 協議作為組件安裝在網絡的網絡適配器的屬性中時,此命令才可用。 2.ping 作用: 1.用來檢測網絡的連通情況和分析網絡速度 2.根據域名得到服務器 IP …

用戶態到內核態:Linux信號傳遞的九重門(二)

1. 保存信號 1.1. 信號其他相關常見概念 實際執?信號的處理動作稱為信號遞達(Delivery)。 信號從產?到遞達之間的狀態,稱為信號未決(Pending)。 進程可以選擇阻塞 (Block )某個信號。 被阻塞的信號產?時將保持在未決狀態,直到進程解除對此信號的阻塞,才執?遞達的動作。 1.…

tar -zxvf jdk-8u212-linux-x64.tar.gz -C /opt/module/這個代碼的解釋

tar -zxvf jdk-8u212-linux-x64.tar.gz -C /opt/module/ 這條命令的解釋如下: 1. tar:這是 Linux 系統中用于歸檔和壓縮文件的命令行工具。 2. -z:表示通過 gzip 壓縮格式來處理文件,因為文件 jdk-8u212-linux-x64.tar.gz 是一個經…

SysAid On-Prem XML注入漏洞復現(CVE-2025-2776)

免責申明: 本文所描述的漏洞及其復現步驟僅供網絡安全研究與教育目的使用。任何人不得將本文提供的信息用于非法目的或未經授權的系統測試。作者不對任何由于使用本文信息而導致的直接或間接損害承擔責任。如涉及侵權,請及時與我們聯系,我們將盡快處理并刪除相關內容。 前…

Nginx的增強與可視化!OpenResty Manager - 現代化UI+高性能反向代理+安全防護

以下是對OpenResty Manager的簡要介紹: OpenResty Manager (Nginx 增強版),是一款容易使用、功能強大且美觀的反向代理工具 ,可以作為OpenResty Edge 的開源替代品基于 OpenResty 開發,支持并繼承 OpenRes…