目錄
前言
概述
主動阻塞/喚醒
代碼示例
實現
為什么必須在同步塊中使用
計時等待是如何實現的
被動阻塞/喚醒
為什么要有被動阻塞/喚醒
實現(鎖升級)
前言
JAVA多線程相關的內容很多很雜,但工作中用到的頻率不高,用到的面也不全,導致是學了又忘忘了又學,在面對并發的場景下沒辦法很好的去解決問題,我相信不止一個人會存在以上這樣的情況。出現這樣的問題,是因為沒有對多線程建立成體系的認識。接下來博主就將用一個系列去帶大家去建立這個體系,博主有信心,這個系列能讓大家從此對JAVA多線程體系有絲滑且根深蒂固的認識。
JAVA多線程部分的內容,無非分為三部分:
-
線程的操作
-
線程狀態模型
-
線程基礎操作:創建線程、阻塞/喚醒、等待/喚醒
-
-
線程安全問題,即JMM
-
一系列的線程同步工具、線程編排工具
本文先講第一部分:線程的操作。
當然這一部分只會講線程的狀態操作,不會去將怎么創建線程、結束線程這些基礎操作,相信大家都有一定基礎看這篇文章才是有益的。
概述
線程的操作這一部分無非內容就是:
-
JAVA中的線程狀態模型
-
線程的基本操作(新建、阻塞、等待)。
如果只是按照順序來聊一遍,那和市面上大多數教程也沒什么區別了,我們要建立關于線程操作這一部分的體系認識,才能從思想上徹底吃透,做到內化于心,這樣怎么都不會忘記。JAVA線程操作部分的體系認識:
1.線程狀態的控制是線程的核心操作
線程存在的根本意義本身就在于控制程序的執行,線程上跑的是程序,通過控制線程狀態來實現對程序執行的控制,想暫停執行就阻塞當前線程,想要繼續執行就讓當前線程處于“就緒”狀態去等待CPU的時間片。所以線程最核心的就是對于它的控制操作,即線程的狀態操作。
2.JAVA的線程狀態也是基于操作系統的原生狀態,只是根據自己的需要將阻塞拆分成了多種
JAVA作為一門編程語言,它是運行在操作系統上的,所以他的線程對應的就是操作系統的真實線程,操作系統原生的線程狀態有三種:就緒、運行、阻塞。JAVA自然不能違背這三種基礎狀態,JAVA只是將阻塞狀態拆成了幾種:輕量級阻塞(等待、計時等待)、重量級阻塞(阻塞)。
為什么要拆成輕量級阻塞和重量級阻塞?JAVA作為一門面向應用開發的語言,要提供線程操控能力,就要允許開發者來手動阻塞/喚醒線程,輕量級阻塞就是是給開發者調用Object的wait()和notify()來手動阻塞/喚醒線程的。重量級阻塞是因為手動操作阻塞/喚醒需要一個絕對線程安全的環境,即需要一個同步塊,JDK提供了synchronized 原語,用來保證資源絕對被單一線程持有,從而創造出一個同步塊出來。可以理解為兩者底層都是調用系統調用對線程進行了真正的阻塞,輕量級阻塞是主動阻塞,重量級阻塞是由JVM層面來控制的阻塞。
3.線程的核心操作是阻塞/喚醒
在JAVA的多線程編程中,我們無非就是通過操作線程“阻塞/喚醒”來實現對線程的控制,這里面無非可做的就兩件事:
-
通過wait()和notify來實現主動的阻塞/喚醒
-
通過synchronized關鍵字來實現被動的阻塞/喚醒
所以搞清楚JAVA線程的阻塞,其實就搞明白了JAVA多線程的核心操作。
主動阻塞/喚醒
代碼示例
api:
-
wait(),等待notify/notifyAll喚醒
-
wait(long timeout),計時等待,到時自動喚醒,也可被notify/notifyAll喚醒
-
notify,喚醒,隨機選擇一個阻塞的線程喚醒
-
notifyAll,喚醒所有阻塞的線程,讓他們去自由爭搶
public class WaitNotifyExample {private final Object lock = new Object();private boolean condition = false; ?public void producer() throws InterruptedException {synchronized (lock) {System.out.println("生產者線程開始...");// 模擬生產耗時Thread.sleep(2000);condition = true;System.out.println("生產者完成,通知消費者...");lock.notify(); // 或者使用 notifyAll() 喚醒所有等待線程}} ?public void consumer() throws InterruptedException {synchronized (lock) {System.out.println("消費者線程開始,等待通知...");while (!condition) { // 使用 while 防止虛假喚醒lock.wait(); // 釋放鎖并等待}System.out.println("消費者被喚醒,繼續執行...");condition = false;}} ?public static void main(String[] args) {WaitNotifyExample example = new WaitNotifyExample();new Thread(() -> {try {example.consumer();} catch (InterruptedException e) {e.printStackTrace();}}).start();new Thread(() -> {try {example.producer();} catch (InterruptedException e) {e.printStackTrace();}}).start();} }
實現
主動的阻塞/喚醒,是為了提供靈活的線程編排能力,能靈活的控制線程的執行進度,既然要足夠靈活,那么就要求線程能被主動阻塞在任何地方,于是JVM選擇能讓線程被阻塞在任何類上。所以JVM會為每個對象維護一個監視器(Monitor),Java的wait()和notify()機制是通過JVM底層的監視器(Monitor)實現的。
class ObjectMonitor {private Thread owner; ? ? ? ? ? ? ?// 當前持有鎖的線程private int recursion; ? ? ? ? ? ? // 重入次數private Queue<Thread> entryList; ? // 等待獲取鎖的線程隊列private Queue<Thread> waitSet; ? ? // 等待條件的線程集合private Object object; ? ? ? ? ? ? // 關聯的Java對象private PlatformEvent platformEvent; // 平臺相關的同步原語 }
【question】為什么要有兩個隊列?
waitSet里面存的是blocked(阻塞狀態)的線程,entryList里面存的是可執行的線程,等待操作系統分配時間片。被阻塞的線程由waitSet喚醒,進入entryList,等待操作系統時間片來執行。
wait()時的隊列操作:
-
保存狀態:保存當前線程的鎖重入信息
-
釋放鎖:將監視器的owner設為null,重入計數歸零
-
加入waitSet:將線程加入等待集合隊列,狀態變為WAITING
-
系統調用:調用操作系統原語阻塞線程
-
移出調度:線程從操作系統的運行隊列中移除
notify()時的隊列操作:
-
檢查waitSet:查看是否有線程在等待
-
選擇線程:從waitSet隊列中選擇一個等待線程
-
移動隊列:將選中線程從waitSet移到entryList
-
改變狀態:線程狀態從WAITING變為BLOCKED
-
系統調用:調用操作系統原語喚醒線程
為什么必須在同步塊中使用
private final Object lock = new Object();private boolean flag = false;//錯誤的并發代碼(偽代碼,實際會拋異常)public void problematicScenario() {// 線程A執行:if (!flag) { // 檢查條件// 此時發生線程切換// 線程B執行并設置flag=true,調用notify()// 線程A恢復執行,調用wait() -> 永遠等待!lock.wait(); // 錯過了通知,永遠阻塞}}//正確的同步版本public void correctScenario() throws InterruptedException {synchronized (lock) {// 檢查和等待是原子操作while (!flag) {lock.wait(); // 不會錯過通知}}}
計時等待是如何實現的
核心流程:
-
驗證監視器所有權 - 檢查當前線程是否持有鎖
-
釋放監視器鎖 - 原子性地釋放對象鎖
-
加入等待隊列 - 線程加入對象的wait_set隊列
-
系統級阻塞 - 調用操作系統原語(futex/park/wait)阻塞線程
-
超時管理 - 啟動定時器監控超時
-
等待喚醒 - 等待notify/notifyAll調用或超時到期
-
重新競爭鎖 - 被喚醒后重新獲取監視器鎖
-
恢復執行 - 繼續執行wait()后面的代碼
如何實現超時管理:
-
操作系統負責超時 (主流實現)
-
JVM調用帶超時參數的系統調用
-
操作系統內核維護定時器
-
內核自動喚醒超時的線程
被動阻塞/喚醒
為什么要有被動阻塞/喚醒
被動阻塞可以理解為JDK提供的原語級別的能力,用來保證資源絕對被單一線程持有。
為什么要提供這種原語級別的能力:
因為我們仔細想想就能想到純靠編碼去進行主動阻塞是無法保證線程安全的。不管什么寫法都不能保證wait()一定是線程安全的。所以JDK要自己提供一個原語來保證創造一個線程安全的環境,也就是創造一個同步區域、同步塊。這就是JDK提供的(被動阻塞/喚醒)同步原語:synchronized關鍵字。
實現(鎖升級)
synchronized 關鍵字用于實現線程同步,它可以保證同一時刻只有一個線程執行被synchronized 修飾的代碼塊或方法。
底層實現原理
-
對象頭(Mark Word) 每個 Java 對象在內存中都有一個對象頭,對象頭中包含鎖信息,用于實現 synchronized。
-
鎖的四種狀態 無鎖狀態:對象未被任何線程鎖定 偏向鎖:偏向第一個訪問對象的線程,減少同一線程獲取鎖的開銷 輕量級鎖:當存在多個線程競爭但競爭不激烈時使用 重量級鎖:當線程競爭激烈時,會阻塞未獲取到鎖的線程
-
鎖升級過程 無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖(單向升級)
無鎖:
當同步代碼首次被一個線程訪問,那么就會在Mark Word記錄該線程的ID,從無鎖狀態(001)變成偏向鎖(101)。
偏向鎖:
當下一次同步代碼被訪問時,那么就會檢測該線程ID與鎖的Mark Word 中的線程ID是否是相同。
相同:則直接進入同步代碼,因為之前沒有釋放鎖
不同:表示發生了競爭,會嘗試使用CAS來替換Mark Word里面的線程ID。競爭成功則會替換Mark Word 里面的線程ID,競爭失敗可能會變成輕量級鎖。
輕量級鎖:
當有第二個線程嘗試獲取鎖時,偏向鎖升級為輕量級鎖,兩個線程自由爭搶,沒搶到的就CAS自旋等待。
重量級鎖:
當自旋超過一定次數(默認10次)或等待線程數超過CPU核心數的一半,鎖升級為重量級鎖,這時候會指向一個監視器對象,這個監視器對象用集合的形式,來登記和管理排隊的線程。