目錄
前言
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()方法:讓指定的程序進入阻塞狀態
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個字
本來筆者想接著介紹阻塞隊列的,看來只能留到下次了!
筆者的風格是每一步都會寫的很詳細,因為筆者覺得自己天賦不佳,需要在學會的時候記錄的越詳細越好,方便讀者查閱和調用
希望筆者如此之高質量的博客能幫助到你我他!