多線程編程的簡單案例——單例模式[多線程編程篇(3)]

目錄

前言

1.wati() 和 notify()

wait() 和 notify() 的產生原因

如何使用wait()和notify()?

?案例一:單例模式?

?餓漢式寫法:

?懶漢式寫法?

對于它的優化

?再次優化

結尾?

前言

如何簡單的去使用jconsloe 查看線程 (多線程編程篇1)_eclipse查看線程-CSDN博客

淺談Thread類及常見方法與線程的狀態(多線程編程篇2)_thread.join() 和thread.get()-CSDN博客

這是系列的第三篇博客,這篇博客筆者想結合自己的學習經歷,分享幾個多線程編程的簡單案例,幫助讀者們更快的理解多線程編程,也非常感激能耐心閱讀本系列博客的讀者們!

本篇博客的內容如下,您可以通過目錄導航直接傳送過去

1.介紹wait()和notify()這兩個方法

2.介紹單例模式

廢話不多說,讓我們開始吧,希望我們在知識的道路上越走越遠!

博客中出現的參考圖都是筆者手畫或者截圖的的

代碼示例也是筆者手敲的!

影響雖小,但請勿抄襲

1.wati() 和 notify()

wait() 和 notify() 的產生原因

在多線程編程中,多個線程同時讀寫共享資源非常常見。假設兩個線程要交替操作一個數據,比如:

  • 線程A:負責生產數據;

  • 線程B:負責消費數據。

如果沒有協調機制,線程A和線程B的執行順序完全由CPU調度,極有可能出現這種情況:

  • 線程B執行時,發現A還沒生產好;

  • 線程A剛生產好,B卻還沒來消費。

這樣會出現資源使用錯誤,甚至死循環。

所以,Java提供了 wait()notify(),解決線程之間通信的問題,幫助程序做到:

?一個線程在條件不滿足時,自動等待。
?另一個線程操作完后,主動喚醒等待的線程。

這種機制,叫做等待-通知機制"。

具體來說:

wait()方法:讓指定的程序進入阻塞狀態

wait 結束等待的條件 :
1.其他線程調用該對象的 notify 方法 .
2.wait 等待時間超時 (wait 方法提供一個帶有 timeout 參數的版本 , 來指定等待時間 ).
3.其他線程調用該等待線程的 interrupted 方法 , 導致 wait 拋出 InterruptedException 異常 .

notify()方法:喚醒對應的處在阻塞狀態的線程.

舉個生活中的例子:

假設你去銀行取號排隊:

  • 你取號后坐在椅子上等待(相當于調用 wait() 進入等待狀態)。

  • 銀行的叫號系統喊你的號碼時,你再去窗口辦理業務(相當于 notify() 喚醒你)。

如果沒有這個等待機制,你可能得不停地站在窗口問“輪到我了嗎?什么時候才能到我啊?前面的人能不能tm快點啊!”(浪費CPU資源)

有了 wait()notify(),就能讓線程“高效地等待”而不是死循環輪詢

如何使用wait()和notify()?

OK了解了他們的概念和作用,接下來,筆者將介紹如何使用wait()和notify()

首先,讀者們需要了解一些前置知識

第一:根據源碼文檔,wait() 方法在調用時,必須處理 InterruptedException
因此使用時要么用 try-catch 捕獲,要么在方法上聲明 throws,否則代碼無法通過編譯。

第二:wait() 和 notify() 方法并不是定義在 Thread 類中,而是屬于 Object 類的方法。
所以在實際使用中,我們通常需要先創建一個 Object 對象,通過這個對象來調用 wait()和 notify(),并且配合 synchronized 關鍵字一起使用,確保線程安全。

請看一組示例代碼:

public class Demo
{public static void main(String[] args) {Object  ob = new Object();Object  lock = new Object();Thread thread1 = new Thread(() ->{synchronized (ob){System.out.println("wait 之前");try {ob.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("進入了");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("wait 之后");});
wait 做的事情:
使當前執行代碼的線程進行等待. (把線程放到等待隊列中)
釋放當前的鎖
滿足一定條件時被喚醒, 重新嘗試獲取這個鎖.Thread thread2 = new Thread(()->{try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (ob){System.out.println("通知了");ob.notify();}});thread1.start();thread2.start();}
}

在使用 wait()notify() 這兩個方法時,有一個非常重要的前提條件:

調用它們時,必須先持有調用對像的鎖,而且必須時同一個對像,否則會拋出異常

我們一定要保證,哪個對像調用了wati(),哪個對像就要調用notify(),或者也要設置好阻塞時間.?

synchronized (ob) {ob.wait();  //  正確,線程1的鎖對象是 ob
}synchronized (lock) {ob.notify();  //  錯誤,線程2的鎖對象是 lock,調用 notify 卻針對 ob
}
錯誤寫法
synchronized (ob) {ob.wait();  //  正確,線程1的鎖對象是 ob
}synchronized (ob) {ob.notify();  
正確寫法


?案例一:單例模式?

?單例模式是一種設計模式

它保證了一個類在內存中永遠只會有一個對象實例.并且提供全局訪問點。

舉個例子:

假設你要開發一個系統中的配置文件讀取器,配置文件只需要加載一次,所有模塊都要讀取相同的配置信息。如果每次調用都重新 new 一個對象,不僅浪費內存,而且可能導致配置不一致。
通過單例模式,你可以保證這個讀取器在整個程序運行期間只創建一次,并且全局唯一!?

又或者?比如 JDBC 中的 DataSource 實例就只需要一個!!!

?單例模式也有兩種寫法 :?

1.懶漢式: 只要在需要被實例化的時候,才會被實例化.

2.餓漢式:顧名思義,在類內部創建唯一實例,并且用 private static final 修飾,保證類一旦被加載了,就開始實例化了

?餓漢式寫法:

public class Singleton {// 餓漢單例,類一旦被加載,就開始實例化了// 1?? 在類內部創建唯一實例,并用 `private static final` 修飾private static final Singleton demo = new Singleton();// 2?? 私有構造方法,防止外部創建實例// 靜態代碼塊private Singleton() {System.out.println("Singleton 實例被創建");}// 3?? 提供公共方法獲取實例public static Singleton getInstance() {return demo;}
}

在餓漢式單例中,我們會直接在類內部創建好對象實例,當類加載進內存時,實例就已經完成了初始化。

這是因為我們使用了 static 關鍵字來修飾這個實例,static 屬于類本身,隨著類的加載而初始化。
所以,只要 JVM 加載這個類,單例對象就會被創建,并且保證全局只有一個。

在 Java 中,static 修飾的屬性或方法屬于類本身,而不是某個具體對象。
類被加載到內存時,所有 static 修飾的成員(屬性、方法、代碼塊)會隨類一起初始化,而且只會初始化一次。

也就是說:

  • 類加載時,static 屬性會被分配內存并初始化。

  • static 方法屬于類本身,不依賴對象,可以通過類名.方法名()調用。

我們簡單測試一下:

class  MyTest
{public static void main(String[] args) {Singleton s1 =  Singleton.getInstance();}
}

調用??Singleton.getInstance()的時候,類被加載,demo被初始化,并且??Singleton() 構造方法被執行,打印"Singleton 實例被創建".

?
懶漢式寫法?

類加載的時候不創建實例 . 第一次使用的時候才創建實例 . 我們依據這個思路,寫出來懶漢式單例
public class SingletonLazy {// 1?? 聲明一個靜態變量用來存儲實例private static  SingletonLazy instance;// 2?? 私有構造方法,防止外部創建實例private SingletonLazy() {System.out.println("SingletonLazy 構造方法執行:對象創建成功!");}// 3?? 提供公共的靜態方法來獲取實例,第一次調用時實例化public static SingletonLazy getInstance() {instance = new SingletonLazy();return instance;}

為了測試懶漢和餓漢的不同,我們再寫兩個輔助的靜態方法測試:

public class SingletonLazy {// 1?? 聲明一個靜態變量用來存儲實例private static  SingletonLazy instance;// 2?? 私有構造方法,防止外部創建實例private SingletonLazy() {System.out.println("SingletonLazy 構造方法執行:對象創建成功!");}// 3?? 提供公共的靜態方法來獲取實例,第一次調用時實例化public static SingletonLazy getInstance() {instance = new SingletonLazy();return instance;}static {System.out.println("SingletonLazy 類已加載!");}public static void printf() {System.out.println("調用了靜態方法 printf()");}}

測試一下:

class Test {public static void main(String[] args) {// 不調用 getInstance 只調用靜態方法SingletonLazy.printf();  // 會觸發類加載,但不會創建對象!System.out.println("---------------");// 真正調用 getInstance,才會創建對象SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();}
}

結果如下:

調用靜態方法后,類會被加載,但此時并不會執行構造方法,也就是說對象還沒有被創建。只有當調用?getInstance()? 方法時,程序才會真正實例化對象,執行構造方法,完成對象的創建!

我們還可以做一點優化,我們都知道這是單例模式,?只允許有一個對象實例,那么,只有第一次訪問時才需要被創建,后續就不用再次創建了,因此可以寫成:

public class SingletonLazy {// 1?? 聲明一個靜態變量用來存儲實例private static volatile SingletonLazy instance;// 2?? 私有構造方法,防止外部創建實例private SingletonLazy() {System.out.println("SingletonLazy 構造方法執行:對象創建成功!");}// 3?? 提供公共的靜態方法來獲取實例,第一次調用時實例化public static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();           }return instance;}
}

?如果在單線程編程下,這樣就挑不出毛病了!

對于它的優化

但是,假設在多線程環境下,有復數個線程同時調用??getInstance() ,那么就會創建出多個實例

舉一個具體的例子

一旦程序進入多線程環境,比如存在A、B、C 三個線程,它們幾乎在同一時刻調用 getInstance()方法

在這一瞬間,instance 的確是 null,三個線程會同時通過 if 判斷,然后同時執行 new SingletonLazy(),最終結果就是:

創建了多個實例,破壞了單例模式!!!

因此,我們希望判斷是否為空,以及創建實例,這兩個動作"原子化"——即不會也不能被打斷

怎么辦?聰明的你肯定想到了,加鎖!

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

加完鎖以后,剛剛的情況就會變為:

1.假設程序運行在多線程環境下,A、B、C 三個線程幾乎在同一時間,調用了 getInstance() 方法。

2.在這一瞬間,instance 的確是 null,于是三個人一起沖進來,準備創建對象。但是!因為這里加了 synchronized,所以三個線程必須搶鎖,只有一個幸運兒能搶到,比如A線程。

3.然后A線程釋放鎖,B、C線程后面排隊進來,發現 instance 已經不再是 null,所以它們就啥也不干,直接返回已有的實例。

4.這樣一來,就保證了全局唯一實例,不會被多線程同時創建多個,單例模式真正實現了!

?再次優化

不過啊,雖然上面這種“方法加鎖”確實解決了多線程下的安全問題——只要一個線程進來了,其他線程就乖乖排隊,等著用同一個實例,表面上看沒毛病。

但是!問題又來了:

每次調用 getInstance(),都要加鎖。
不管 instance 有沒有被創建,線程都得卡著 synchronized 排隊。

想一想——如果我已經拿到實例了,后面無數次調用其實都只是想用一下這個對象,根本不需要再創建,可還是得老老實實搶鎖,這效率能不低嗎? 畢竟,加鎖的開銷也不小了.

所以,聰明的程序員又想了個辦法,叫:

雙重檢查鎖(Double-Check Locking),簡稱 DCL。

核心思路就一句話:

先檢查,不滿足再加鎖,鎖住后再檢查,確認安全后再創建。

也就是說,外面先檢查一次,里面再檢查一次,這樣只有在 instance 真正等于 null 的時候,才會走到創建對象的邏輯,其他時候,直接跳過鎖,快速返回。

public class SingletonLazy {// 加上 volatile,防止指令重排序private static volatile SingletonLazy instance;private SingletonLazy() {System.out.println("SingletonLazy 構造方法執行:對象創建成功!");}public static SingletonLazy getInstance() {if (instance == null) {  // 第一次檢查synchronized (SingletonLazy.class) {if (instance == null) {  // 第二次檢查instance = new SingletonLazy();}}}return instance;}
}

而且還有個小細節,volatile 關鍵字也別忘了加上!

因為 Java 內存模型中,new 操作可能會被“重排序”

那么,還是剛剛ABC三線程競爭的例子:

1.

A、B、C 三個線程同時調用 getInstance(),一起執行第一次 if (instance == null)

2.?假設 instance 真的為 null,于是三個線程都準備往下走。

3.

A、B、C 到達 synchronized 這里,開始搶鎖。假設A贏了,進入同步代碼塊。

A 再次執行第二次 if (instance == null),發現確實為空,于是創建 new SingletonLazy()
A 創建完成后,釋放鎖。

4.

B、C 排隊進來,再次檢查 if (instance == null),發現已經不為空了,直接跳過創建,返回已存在的實例。?

這樣對比普通加鎖的好處是,實例化以后,先判斷一下是否是空,而不是多個線程直接去競爭鎖導致資源浪費

總結一句話:
DCL的好處就是,實例化之后,線程們先看一眼:
"對象在不在?"
在,就立刻用!
不在,才排隊搶鎖。

相比“每次都搶鎖”的方式,DCL大幅減少了資源浪費,尤其適合多線程訪問頻繁的場景。

完整代碼:

public class SingletonLazy {// 1?? 聲明一個靜態變量用來存儲實例private static volatile SingletonLazy instance;// 2?? 私有構造方法,防止外部創建實例private SingletonLazy() {System.out.println("SingletonLazy 構造方法執行:對象創建成功!");}// 3?? 提供公共的靜態方法來獲取實例,第一次調用時實例化public static SingletonLazy getInstance() {if(instance == null){synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}}}return instance;}
// 外層 if 的作用:
// 避免已經實例化對象的情況下,仍然加鎖。因為加鎖是一種消耗性能的操作,
// 所以外層先判斷,能直接返回就直接返回,提高效率。// 內層 if 的作用:
// 防止多個線程在 instance == null 的情況下,同時進入同步代碼塊,
// 搶鎖后,重復創建實例。內層 if 可以保證只有第一個搶到鎖的線程會創建實例。// 假設 instance 初始為 null,兩個線程 A 和 B 幾乎同時調用 getInstance():
// 【第一階段:外層 if 判斷(無鎖)】
// - 線程A發現 instance == null,進入同步塊等待搶鎖。
// - 線程B也發現 instance == null,也準備進入同步塊等待搶鎖。// 【第二階段:嘗試獲取鎖】
// - 線程A搶到 synchronized(SingletonLazy.class) 的鎖,進入同步塊,開始執行內層代碼。
// - 線程B未搶到鎖,必須等待線程A釋放鎖,掛起等待。// 【第三階段:內層 if 判斷】
// - 線程A在內層再次檢查 instance 是否為 null,
//   如果確實是 null,就創建 SingletonLazy 實例。
// - 線程A釋放鎖,線程B接著搶到鎖。// 【第四階段:線程B再次檢查】
// - 線程B進入同步塊,內層 if 判斷時,發現 instance 已經不是 null,
//   所以不會再創建新對象,直接返回已存在的實例。// 【總結】
// 這樣寫的雙重檢查機制,既保證了線程安全,
// 又避免每次都去加鎖,提升了性能!// 輔助方法,觀察類是否加載static {System.out.println("SingletonLazy 類已加載!");}public static void printf() {System.out.println("調用了靜態方法 printf()");}
}class Test {public static void main(String[] args) {// 不調用 getInstance 只調用靜態方法SingletonLazy.printf();  // 會觸發類加載,但不會創建對象!System.out.println("---------------");// 真正調用 getInstance,才會創建對象SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();}
}

結尾?

寫到這里的時候,大約花費了筆者120分鐘,寫了8145個字

本來筆者想接著介紹阻塞隊列的,看來只能留到下次了!

筆者的風格是每一步都會寫的很詳細,因為筆者覺得自己天賦不佳,需要在學會的時候記錄的越詳細越好,方便讀者查閱和調用

希望筆者如此之高質量的博客能幫助到你我他!

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

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

相關文章

pytorch基本操作2

torch.clamp 主要用于對張量中的元素進行截斷(clamping),將其限制在一個指定的區間范圍內。 函數定義 torch.clamp(input, minNone, maxNone) → Tensor 參數說明 input 類型:Tensor 需要進行截斷操作的輸入張…

一次制作參考網雜志的閱讀書源的實操經驗總結(附書源)

文章目錄 一、背景介紹二、書源文件三、詳解制作書源(一)打開Web服務(二)參考網結構解釋(三)閱讀書源 基礎(四)閱讀書源 發現(五)閱讀書源 詳細(六…

并發設計模式實戰系列(2):領導者/追隨者模式

🌟 ?大家好,我是摘星!? 🌟 今天為大家帶來的是并發設計模式實戰系列,第二章領導者/追隨者(Leader/Followers)模式,廢話不多說直接開始~ 目錄 領導者/追隨者(Leader/…

自求導實現線性回歸與PyTorch張量詳解

目錄 前言一、自求導的方法實現線性回歸1.1自求導的方法實現線性回歸的理論講解1.1.1 線性回歸是什么?1.1.2線性回歸方程是什么?1.1.3散點輸入1.2參數初始化1.2.1 參數與超參數1.2.1.1 參數定義1.2.1.2 參數內容1.2.1.3 超參數定義1.2.1.4 超參數內容1.…

2025年機電一體化、機器人與人工智能國際學術會議(MRAI 2025)

重要信息 時間:2025年4月25日-27日 地點:中國濟南 官網:http://www.icmrai.org 征稿主題 機電一體化機器人人工智能 傳感器和執行器 3D打印技術 智能控制 運動控制 光電系統 光機電一體化 類人機器人 人機界面 先進的運動控制 集成制造系…

線性代數 | 知識點整理 Ref 3

注:本文為 “線性代數 | 知識點整理” 相關文章合輯。 因 csdn 篇幅合并超限分篇連載,本篇為 Ref 3。 略作重排,未整理去重。 圖片清晰度限于引文原狀。 如有內容異常,請看原文。 《線性代數》總復習要點、公式、重要結論與重點釋…

CFD中的動量方程非守恒形式詳解

在計算流體力學(CFD)中,動量方程可以寫成守恒形式和非守恒形式,兩者在數學上等價,但推導方式和應用場景不同。以下是對非守恒形式的詳細解釋: 1. 動量方程的守恒形式 首先回顧守恒形式的動量方程&#xff…

Leetcode 1504. 統計全 1 子矩形

1.題目基本信息 1.1.題目描述 給你一個 m x n 的二進制矩陣 mat ,請你返回有多少個 子矩形 的元素全部都是 1 。 1.2.題目地址 https://leetcode.cn/problems/count-submatrices-with-all-ones/description/ 2.解題方法 2.1.解題思路 單調棧 時間復雜度&…

【Docker】運行錯誤提示 unknown shorthand flag: ‘d‘ in -d ----詳細解決方法

使用docker拉取Dify的時候遇到錯誤 錯誤提示 unknown shorthand flag: d in -dUsage: docker [OPTIONS] COMMAND [ARG...]錯誤原因解析 出現 unknown shorthand flag: d in -d 的根本原因是 Docker 命令格式與當前版本不兼容,具體分為以下兩種情況: 新…

華為OD機試真題——攀登者2(2025A卷:200分)Java/python/JavaScript/C++/C語言/GO六種最佳實現

2025 A卷 200分 題型 本文涵蓋詳細的問題分析、解題思路、代碼實現、代碼詳解、測試用例以及綜合分析; 并提供Java、python、JavaScript、C、C語言、GO六種語言的最佳實現方式! 2025華為OD真題目錄全流程解析/備考攻略/經驗分享 華為OD機試真題《攀登者2…

qt硬件與軟件通信中 16進制與十進制轉化

1. 首先上代碼, 這是在qt語言上的操作 截取 01 03 0C 00 00 00 00 00 00 00 0C 00 0C 00 0C 93 70 這串16進制數值進行處理,截取這樣一段內容 00 0C 00 0C 00 0C 字節數組轉字符串。從bytearray數組轉換為string. QString CustomTcpSocket::recieveInfo() {QByteArr…

圖形變換算法

一、學習目的 (1)掌握多面體的存儲方法。 (2)掌握圖形的幾何變換及投影變換。 (3)掌握三維形體不同投影方法的投影圖的生成原理。 (4)掌握多面體投影圖繪制的編程方法。 二、學…

【JAVAFX】自定義FXML 文件存放的位置以及使用

情況 1:FXML 文件與調用類在同一個包中(推薦) 假設類 MainApp 的包是 com.example,且 FXML 文件放在 resources/com/example 下: 項目根目錄 ├── src │ └── sample │ └── Main.java ├── src/s…

Ubuntu20.04安裝企業微信

建議先去企業微信官網看一下有沒有linux版本,沒有的話在按如下方式安裝,不過現在是沒有的。 方案 1、使用docker容器 2、使用deepin-wine 3、使用星火應用商店 4. 使用星火包deepin-wine 5、使用ukylin-wine 本人對docker不太熟悉,現…

CSS appearance 屬性:掌握UI元素的原生外觀

在現代網頁設計中,為了達到一致的用戶體驗,我們有時需要讓HTML元素模仿操作系統的默認控件樣式。CSS中的appearance屬性提供了一種簡便的方式來控制這些元素是否以及如何顯示其默認外觀。本文將詳細介紹appearance屬性,并通過實際代碼示例來展…

十四、C++速通秘籍—函數式編程

目錄 上一章節: 一、引言 一、函數式編程基礎 三、Lambda 表達式 作用: Lambda 表達式捕獲值的方式: 注意: 四、函數對象 函數對象與普通函數對比: 五、函數適配器 1、適配普通函數 2、適配 Lambda 表達式 …

大模型Rag-指令調度

本文主要記錄根據用戶問題指令,基于大模型做Rag,匹配最相關描述集進行指令調度,可用于匹配后端接口以及展示答案及圖表等。 1.指令查詢處理邏輯 1.實現思路 指令識別:主要根據用戶的問題q計算與指令描述集is [i0, ... , im]和指…

音視頻學習 - ffmpeg 編譯與調試

編譯 環境 macOS Ventrua 13.4 ffmpeg 7.7.1 Visual Studio Code Version: 1.99.0 (Universal) 操作 FFmpeg 下載源碼 $ cd ffmpeg-x.y.z $ ./configure nasm/yasm not found or too old. Use --disable-x86asm for a crippled build.If you think configure made a mistake…

golang-常見的語法錯誤

https://juejin.cn/post/6923477800041054221 看這篇文章 Golang 基礎面試高頻題詳細解析【第一版】來啦~ 大叔說碼 for-range的坑 func main() { slice : []int{0, 1, 2, 3} m : make(map[int]*int) for key, val : range slice {m[key] &val }for k, v : …

音視頻之H.265/HEVC預測編碼

H.265/HEVC系列文章: 1、音視頻之H.265/HEVC編碼框架及編碼視頻格式 2、音視頻之H.265碼流分析及解析 3、音視頻之H.265/HEVC預測編碼 預測編碼是視頻編碼中的核心技術之一。對于視頻信號來說,一幅圖像內鄰近像素之間有著較強的空間相關性,相鄰圖像之…