並發歷史
在計算機最早期的時候,沒有操作系統,執行程序只需要一種方式,那就是從頭到尾依次執行。任何資源都會為這個程序服務,在計算機使用某些資源時,其他資源就會空閑,就會存在 浪費資源
的情況。
這里說的浪費資源指的是資源空閑,沒有充分使用的情況。
操作系統的出現為我們的程序帶來了 並發性
,操作系統使我們的程序能夠同時運行多個程序,一個程序就是一個進程,也就相當於同時運行多個進程。
操作系統是一個並發系統
,並發性是操作系統非常重要的特征,操作系統具有同時處理和調度多個程序的能力,比如多個 I/O 設備同時在輸入輸出;設備 I/O 和 CPU 計算同時進行;內存中同時有多個系統和用戶程序被啟動交替、穿插地執行。操作系統在協調和分配進程的同時,操作系統也會為不同進程分配不同的資源。
操作系統實現多個程序同時運行解決了單個程序無法做到的問題,主要有下面三點
資源利用率
,我們上面說到,單個進程存在資源浪費的情況,舉個例子,當你在為某個文件夾賦予權限的時候,輸入程序無法 接受外部的輸入字符,只有等到權限賦予完畢后才能接受外部輸入。總的來講,就是在等待程序時無法執行其他工作。如果在等待程序時可以運行另一個程序,那么 將會大大提高資源的利用率。(資源並不會覺得累)因為它不會划水~公平性
,不同的用戶和程序都能夠使用計算機上的資源。一種高效的運行方式是為不同的程序划分時間片來使用資源,但是有 一點需要注意,操作系統可以決定不同進程的優先級。雖然每個進程都有能夠公平享有資源的權利,但是當有一個進程釋放資源后的同時有一個優先級更高的進程搶 奪資源,就會造成優先級低的進程無法獲得資源,進而導致進程飢餓。便利性
,單個進程是是不用通信的,通信的本質就是信息交換
,及時進行信息交換能夠避免信息孤島
,做重復性的工作;任何並發能做的事情,單進程也能夠實現,只不過這種方式效率很低,它是一種順序性
的。
但是,順序編程(也稱為串行編程
)也不是一無是處
的,串行編程的優勢在於其直觀性和簡單性,客觀來講,串行編程更適合我們人腦的思考方式,但是我們並不會滿足於順序編程,we want it more!!! 。資源利用率、公平性和便利性促使着進程出現的同時,也促使着線程
的出現。
如果你還不是很理解進程和線程的區別的話,那么我就以我多年操作系統的經驗(吹牛逼,實則半年)來為你解釋一下:進程是一個應用程序,而線程是應用程序中的一條順序流。
進程中會有多個線程來完成一些任務,這些任務有可能相同有可能不同。每個線程都有自己的執行順序。
每個線程都有自己的棧空間,這是線程私有的,還有一些其他線程內部的和線程共享的資源,如下所示。
在計算機中,一般堆棧指的就是棧,而堆指的才是堆
線程會共享進程范圍內的資源,例如內存和文件句柄,但是每個線程也有自己私有的內容,比如程序計數器、棧以及局部變量。下面匯總了進程和線程共享資源的區別
線程是一種輕量級
的進程,輕量級體現在線程的創建和銷毀要比進程的開銷小很多。
注意:任何比較都是相對的。
在大多數現代操作系統中,都以線程為基本的調度單位,所以我們的視角着重放在對線程
的探究。
線程
什么是多線程
多線程意味着你能夠在同一個應用程序中運行多個線程,我們知道,指令是在 CPU 中執行的,多線程應用程序就像是具有多個 CPU 在同時執行應用程序的代碼。
其實這是一種假象,線程數量並不等於 CPU 數量,單個 CPU 將在多個線程之間共享 CPU 的時間片,在給定的時間片內執行每個線程之間的切換,每個線程也可以由不同的 CPU 執行,如下圖所示
並發和並行的關系
並發
意味着應用程序會執行多個的任務,但是如果計算機只有一個 CPU 的話,那么應用程序無法同時執行多個的任務,但是應用程序又需要執行多個任務,所以計算機在開始執行下一個任務之前,它並沒有完成當前的任務,只是把狀態 暫存,進行任務切換,CPU 在多個任務之間進行切換,直到任務完成。如下圖所示
並行
是指應用程序將其任務分解為較小的子任務,這些子任務可以並行處理,例如在多個CPU上同時進行。
優勢和劣勢
合理使用線程是一門藝術,合理編寫一道准確無誤的多線程程序更是一門藝術,如果線程使用得當,能夠有效的降低程序的開發和維護成本。
Java 很好的在用戶空間實現了開發工具包,並在內核空間提供系統調用來支持多線程編程,Java 支持了豐富的類庫 java.util.concurrent
和跨平台的內存模型
,同時也提高了開發人員的門檻,並發一直以來是一個高階的主題,但是現在,並發也成為了主流開發人員的必備素質。
雖然線程帶來的好處很多,但是編寫正確的多線程(並發)程序是一件極困難的事情,並發程序的 Bug 往往會詭異地出現又詭異的消失,在當你認為沒有問題的時候它就出現了,難以定位
是並發程序的一個特征,所以在此基礎上你需要有扎實的並發基本功。那么,並發為什么會出現呢?
並發為什么會出現
計算機世界的快速發展離不開 CPU、內存和 I/O 設備的高速發展,但是這三者一直存在速度差異性問題,我們可以從存儲器的層次結構可以看出
CPU 內部是寄存器的構造,寄存器的訪問速度要高於高速緩存
,高速緩存的訪問速度要高於內存,最慢的是磁盤訪問。
程序是在內存中執行的,程序里大部分語句都要訪問內存,有些還需要訪問 I/O 設備,根據漏桶理論來說,程序整體的性能取決於最慢的操作也就是磁盤訪問速度。
因為 CPU 速度太快了,所以為了發揮 CPU 的速度優勢,平衡這三者的速度差異,計算機體系機構、操作系統、編譯程序都做出了貢獻,主要體現為:
- CPU 使用緩存來中和和內存的訪問速度差異
- 操作系統提供進程和線程調度,讓 CPU 在執行指令的同時分時復用線程,讓內存和磁盤不斷交互,不同的
CPU 時間片
能夠執行不同的任務,從而均衡這三者的差異 - 編譯程序提供優化指令的執行順序,讓緩存能夠合理的使用
我們在享受這些便利的同時,多線程也為我們帶來了挑戰,下面我們就來探討一下並發問題為什么會出現以及多線程的源頭是什么
線程帶來的安全性問題
線程安全性是非常復雜的,在沒有采用同步機制
的情況下,多個線程中的執行操作往往是不可預測的,這也是多線程帶來的挑戰之一,下面我們給出一段代碼,來看看安全性問題體現在哪
public class TSynchronized implements Runnable{ static int i = 0; public void increase(){ i++; } @Override public void run() { for(int i = 0;i < 1000;i++) { increase(); } } public static void main(String[] args) throws InterruptedException { TSynchronized tSynchronized = new TSynchronized(); Thread aThread = new Thread(tSynchronized); Thread bThread = new Thread(tSynchronized); aThread.start(); bThread.start(); System.out.println("i = " + i); } }
這段程序輸出后會發現,i 的值每次都不一樣,這不符合我們的預測,那么為什么會出現這種情況呢?我們先來分析一下程序的運行過程。
TSynchronized
實現了 Runnable 接口,並定義了一個靜態變量 i
,然后在 increase
方法中每次都增加 i 的值,在其實現的 run 方法中進行循環調用,共執行 1000 次。
可見性問題
在單核 CPU 時代,所有的線程共用一個 CPU,CPU 緩存和內存的一致性問題容易解決,CPU 和 內存之間
如果用圖來表示的話我想會是下面這樣
在多核時代,因為有多核的存在,每個核都能夠獨立的運行一個線程,每顆 CPU 都有自己的緩存,這時 CPU 緩存與內存的數據一致性就沒那么容易解決了,當多個線程在不同的 CPU 上執行時,這些線程操作的是不同的 CPU 緩存
因為 i 是靜態變量,沒有經過任何線程安全措施的保護,多個線程會並發修改 i 的值,所以我們認為 i 不是線程安全的,導致這種結果的出現是由於 aThread 和 bThread 中讀取的 i 值彼此不可見,所以這是由於 可見性
導致的線程安全問題。
原子性問題
看起來很普通的一段程序卻因為兩個線程 aThread
和 bThread
交替執行產生了不同的結果。但是根源不是因為創建了兩個線程導致的,多線程只是產生線程安全性的必要條件,最終的根源出現在 i++
這個操作上。
這個操作怎么了?這不就是一個給 i 遞增的操作嗎?也就是 i++ => i = i + 1,這怎么就會產生問題了?
因為 i++
不是一個 原子性
操作,仔細想一下,i++ 其實有三個步驟,讀取 i 的值,執行 i + 1 操作,然后把 i + 1 得出的值重新賦給 i(將結果寫入內存)。
當兩個線程開始運行后,每個線程都會把 i 的值讀入到 CPU 緩存中,然后執行 + 1 操作,再把 + 1 之后的值寫入內存。因為線程間都有各自的虛擬機棧和程序計數器,他們彼此之間沒有數據交換,所以當 aThread 執行 + 1 操作后,會把數據寫入到內存,同時 bThread 執行 + 1 操作后,也會把數據寫入到內存,因為 CPU 時間片的執行周期是不確定的,所以會出現當 aThread 還沒有把數據寫入內存時,bThread 就會讀取內存中的數據,然后執行 + 1操作,再寫回內存,從而覆蓋 i 的值,導致 aThread 所做的努力白費。
為什么上面的線程切換會出現問題呢?
我們先來考慮一下正常情況下(即不會出現線程安全性問題的情況下)兩條線程的執行順序
可以看到,當 aThread 在執行完整個 i++ 的操作后,操作系統對線程進行切換,由 aThread -> bThread,這是最理想的操作,一旦操作系統在任意 讀取/增加/寫入
階段產生線程切換,都會產生線程安全問題。例如如下圖所示
最開始的時候,內存中 i = 0,aThread 讀取內存中的值並把它讀取到自己的寄存器中,執行 +1 操作,此時發生線程切換,bThread 開始執行,讀取內存中的值並把它讀取到自己的寄存器中,此時發生線程切換,線程切換至 aThread 開始運行,aThread 把自己寄存器的值寫回到內存中,此時又發生線程切換,由 aThread -> bThread,線程 bThread 把自己寄存器的值 +1 然后寫回內存,寫完后內存中的值不是 2 ,而是 1, 內存中的 i 值被覆蓋了。
我們上面提到 原子性
這個概念,那么什么是原子性呢?
並發編程的原子性操作是完全獨立於任何其他進程運行的操作,原子操作多用於現代操作系統和並行處理系統中。
原子操作通常在內核中使用,因為內核是操作系統的主要組件。但是,大多數計算機硬件,編譯器和庫也提供原子性操作。
在加載和存儲中,計算機硬件對存儲器字進行讀取和寫入。為了對值進行匹配、增加或者減小操作,一般通過原子操作進行。在原子操作期間,處理器可以在同一數據傳輸期間完成讀取和寫入。 這樣,其他輸入/輸出機制或處理器無法執行存儲器讀取或寫入任務,直到原子操作完成為止。
簡單來講,就是原子操作要么全部執行,要么全部不執行。數據庫事務的原子性也是基於這個概念演進的。
有序性問題
在並發編程中還有帶來讓人非常頭疼的 有序性
問題,有序性顧名思義就是順序性,在計算機中指的就是指令的先后執行順序。一個非常顯而易見的例子就是 JVM 中的類加載
這是一個 JVM 加載類的過程圖,也稱為類的生命周期,類從加載到 JVM 到卸載一共會經歷五個階段 加載、連接、初始化、使用、卸載。這五個過程的執行順序是一定的,但是在連接階段,也會分為三個過程,即 驗證、准備、解析 階段,這三個階段的執行順序不是確定的,通常交叉進行,在一個階段的執行過程中會激活另一個階段。
有序性問題一般是編譯器帶來的,編譯器有的時候確實是 好心辦壞事,它為了優化系統性能,往往更換指令的執行順序。
活躍性問題
多線程還會帶來活躍性
問題,如何定義活躍性問題呢?活躍性問題關注的是 某件事情是否會發生。
如果一組線程中的每個線程都在等待一個事件的發生,而這個事件只能由該組中正在等待的線程觸發,這種情況會導致死鎖。
簡單一點來表述一下,就是每個線程都在等待其他線程釋放資源,而其他資源也在等待每個線程釋放資源,這樣沒有線程搶先釋放自己的資源,這種情況會產生死鎖,所有線程都會無限的等待下去。
死鎖的必要條件
造成死鎖的原因有四個,破壞其中一個即可破壞死鎖
- 互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程占用。如果此時還有其它進程請求資源,則請求者只能等待,直至占有資源的進程釋放。
- 請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程占有,此時請求進程阻塞,但又對自己已獲得的其它資源保持占有。
- 不剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
- 循環等待:指在發生死鎖時,必然存在一個進程對應的環形鏈。
換句話說,死鎖線程集合中的每個線程都在等待另一個死鎖線程占有的資源。但是由於所有線程都不能運行,它們之中任何一個資源都無法釋放資源,所以沒有一個線程可以被喚醒。
如果說死鎖很痴情
的話,那么活鎖
用一則成語來表示就是 弄巧成拙
。
某些情況下,當線程意識到它不能獲取所需要的下一個鎖時,就會嘗試禮貌的釋放已經獲得的鎖,然后等待非常短的時間再次嘗試獲取。可以想像一下這個場景:當兩個人在狹路相逢的時候,都想給對方讓路,相同的步調會導致雙方都無法前進。
現在假想有一對並行的線程用到了兩個資源。它們分別嘗試獲取另一個鎖失敗后,兩個線程都會釋放自己持有的鎖,再次進行嘗試,這個過程會一直進行重復。很明顯,這個過程中沒有線程阻塞,但是線程仍然不會向下執行,這種狀況我們稱之為 活鎖(livelock)
。
如果我們期望的事情一直不會發生,就會產生活躍性問題,比如單線程中的無限循環
while(true){...} for(;;){}
在多線程中,比如 aThread 和 bThread 都需要某種資源,aThread 一直占用資源不釋放,bThread 一直得不到執行,就會造成活躍性問題,bThread 線程會產生飢餓
,我們后面會說。
性能問題
與活躍性問題密切相關的是 性能
問題,如果說活躍性問題關注的是最終的結果,那么性能問題關注的就是造成結果的過程,性能問題有很多方面:比如服務時間過長,吞吐率過低,資源消耗過高,在多線程中這樣的問題同樣存在。
在多線程中,有一個非常重要的性能因素那就是我們上面提到的 線程切換
,也稱為 上下文切換(Context Switch)
,這種操作開銷很大。
在計算機世界中,老外都喜歡用 context 上下文這個詞,這個詞涵蓋的內容很多,包括上下文切換的資源,寄存器的狀態、程序計數器等。context switch 一般指的就是這些上下文切換的資源、寄存器狀態、程序計數器的變化等。
在上下文切換中,會保存和恢復上下文,丟失局部性,把大量的時間消耗在線程切換上而不是線程運行上。
為什么線程切換會開銷如此之大呢?線程間的切換會涉及到以下幾個步驟
將 CPU 從一個線程切換到另一線程涉及掛起當前線程,保存其狀態,例如寄存器,然后恢復到要切換的線程的狀態,加載新的程序計數器,此時線程切換實際上就已經完成了;此時,CPU 不在執行線程切換代碼,進而執行新的和線程關聯的代碼。
引起線程切換的幾種方式
線程間的切換一般是操作系統層面需要考慮的問題,那么引起線程上下文切換有哪幾種方式呢?或者說線程切換有哪幾種誘因呢?主要有下面幾種引起上下文切換的方式
- 當前正在執行的任務完成,系統的 CPU 正常調度下一個需要運行的線程
- 當前正在執行的任務遇到 I/O 等阻塞操作,線程調度器掛起此任務,繼續調度下一個任務。
- 多個任務並發搶占鎖資源,當前任務沒有獲得鎖資源,被線程調度器掛起,繼續調度下一個任務。
- 用戶的代碼掛起當前任務,比如線程執行 sleep 方法,讓出CPU。
- 使用硬件中斷的方式引起上下文切換
線程安全性
在 Java 中,要實現線程安全性,必須要正確的使用線程和鎖,但是這些只是滿足線程安全的一種方式,要編寫正確無誤的線程安全的代碼,其核心就是對狀態訪問操作進行管理。最重要的就是最 共享(Shared)
的 和 可變(Mutable)
的狀態。只有共享和可變的變量才會出現問題,私有變量不會出現問題,參考程序計數器
。
對象的狀態可以理解為存儲在實例變量或者靜態變量中的數據,共享意味着某個變量可以被多個線程同時訪問、可變意味着變量在生命周期內會發生變化。一個變量是否是線程安全的,取決於它是否被多個線程訪問。要使變量能夠被安全訪問,必須通過同步機制來對變量進行修飾。
如果不采用同步機制的話,那么就要避免多線程對共享變量的訪問,主要有下面兩種方式
- 不要在多線程之間共享變量
- 將共享變量置為不可變的
我們說了這么多次線程安全性,那么什么是線程安全性呢?
什么是線程安全性
多個線程可以同時安全調用的代碼稱為線程安全的,如果一段代碼是安全的,那么這段代碼就不存在 競態條件
。僅僅當多個線程共享資源時,才會出現競態條件。
根據上面的探討,我們可以得出一個簡單的結論:當多個線程訪問某個類時,這個類始終都能表現出正確的行為,那么就稱這個類是線程安全的。
單線程就是一個線程數量為 1 的多線程,單線程一定是線程安全的。讀取某個變量的值不會產生安全性問題,因為不管讀取多少次,這個變量的值都不會被修改。
原子性
我們上面提到了原子性的概念,你可以把原子性
操作想象成為一個不可分割
的整體,它的結果只有兩種,要么全部執行,要么全部回滾。你可以把原子性認為是 婚姻關系
的一種,男人和女人只會產生兩種結果,好好的
和 說散就散
,一般男人的一生都可以把他看成是原子性的一種,當然我們不排除時間管理(線程切換)
的個例,我們知道線程切換必然會伴隨着安全性問題,男人要出去浪也會造成兩種結果,這兩種結果分別對應安全性的兩個結果:線程安全(好好的)和線程不安全(說散就散)。
競態條件
有了上面的線程切換的功底,那么競態條件也就好定義了,它指的就是兩個或多個線程同時對一共享數據進行修改,從而影響程序運行的正確性時,這種就被稱為競態條件(race condition) ,線程切換是導致競態條件出現的誘導因素,我們通過一個示例來說明,來看一段代碼
public class RaceCondition { private Signleton single = null; public Signleton newSingleton(){ if(single == null){ single = new Signleton(); } return single; } }
在上面的代碼中,涉及到一個競態條件,那就是判斷 single
的時候,如果 single 判斷為空,此時發生了線程切換,另外一個線程執行,判斷 single 的時候,也是空,執行 new 操作,然后線程切換回之前的線程,再執行 new 操作,那么內存中就會有兩個 Singleton 對象。
加鎖機制
在 Java 中,有很多種方式來對共享和可變的資源進行加鎖和保護。Java 提供一種內置的機制對資源進行保護:synchronized
關鍵字,它有三種保護機制
- 對方法進行加鎖,確保多個線程中只有一個線程執行方法;
- 對某個對象實例(在我們上面的探討中,變量可以使用對象來替換)進行加鎖,確保多個線程中只有一個線程對對象實例進行訪問;
- 對類對象進行加鎖,確保多個線程只有一個線程能夠訪問類中的資源。
synchronized 關鍵字對資源進行保護的代碼塊俗稱 同步代碼塊(Synchronized Block)
,例如
synchronized(lock){ // 線程安全的代碼 }
每個 Java 對象都可以用做一個實現同步的鎖,這些鎖被稱為 內置鎖(Instrinsic Lock)
或者 監視器鎖(Monitor Lock)
。線程在進入同步代碼之前會自動獲得鎖,並且在退出同步代碼時自動釋放鎖,而無論是通過正常執行路徑退出還是通過異常路徑退出,獲得內置鎖的唯一途徑就是進入這個由鎖保護的同步代碼塊或方法。
synchronized 的另一種隱含的語義就是 互斥
,互斥意味着獨占
,最多只有一個線程持有鎖,當線程 A 嘗試獲得一個由線程 B 持有的鎖時,線程 A 必須等待或者阻塞,直到線程 B 釋放這個鎖,如果線程 B 不釋放鎖的話,那么線程 A 將會一直等待下去。
線程 A 獲得線程 B 持有的鎖時,線程 A 必須等待或者阻塞,但是獲取鎖的線程 B 可以重入,重入的意思可以用一段代碼表示
public class Retreent { public synchronized void doSomething(){ doSomethingElse(); System.out.println("doSomething......"); } public synchronized void doSomethingElse(){ System.out.println("doSomethingElse......"); }
獲取 doSomething() 方法鎖的線程可以執行 doSomethingElse() 方法,執行完畢后可以重新執行 doSomething() 方法中的內容。鎖重入也支持子類和父類之間的重入,具體的我們后面會進行介紹。
volatile
是一種輕量級的 synchronized
,也就是一種輕量級的加鎖方式,volatile 通過保證共享變量的可見性來從側面對對象進行加鎖。可見性的意思就是當一個線程修改一個共享變量時,另外一個線程能夠 看見
這個修改的值。volatile 的執行成本要比 synchronized
低很多,因為 volatile 不會引起線程的上下文切換。
我們還可以使用原子類
來保證線程安全,原子類其實就是 rt.jar
下面以 atomic
開頭的類
除此之外,我們還可以使用 java.util.concurrent
工具包下的線程安全的集合類來確保線程安全,具體的實現類和其原理我們后面會說。
可以使用不同的並發模型來實現並發系統,並發模型說的是系統中的線程如何協作完成並發任務。不同的並發模型以不同的方式拆分任務,線程可以以不同的方式進行通信和協作。
競態條件和關鍵區域
競態條件是在關鍵代碼區域發生的一種特殊條件。關鍵區域是由多個線程同時執行的代碼部分,關鍵區域中的代碼執行順序會對造成不一樣的結果。如果多個線程執行一段關鍵代碼,而這段關鍵代碼會因為執行順序不同而造成不同的結果時,那么這段代碼就會包含競爭條件。
並發模型和分布式系統很相似
並發模型其實和分布式系統模型非常相似,在並發模型中是線程
彼此進行通信,而在分布式系統模型中是 進程
彼此進行通信。然而本質上,進程和線程也非常相似。這也就是為什么並發模型和分布式模型非常相似的原因。
分布式系統通常要比並發系統面臨更多的挑戰和問題比如進程通信、網絡可能出現異常,或者遠程機器掛掉等等。但是一個並發模型同樣面臨着比如 CPU 故障、網卡出現問題、硬盤出現問題等。
因為並發模型和分布式模型很相似,因此他們可以相互借鑒,例如用於線程分配的模型就類似於分布式系統環境中的負載均衡模型。
其實說白了,分布式模型的思想就是借鑒並發模型的基礎上推演發展來的。
認識兩個狀態
並發模型的一個重要的方面是,線程是否應該共享狀態
,是具有共享狀態
還是獨立狀態
。共享狀態也就意味着在不同線程之間共享某些狀態
狀態其實就是數據
,比如一個或者多個對象。當線程要共享數據時,就會造成 競態條件
或者 死鎖
等問題。當然,這些問題只是可能會出現,具體實現方式取決於你是否安全的使用和訪問共享對象。
獨立的狀態表明狀態不會在多個線程之間共享,如果線程之間需要通信的話,他們可以訪問不可變的對象來實現,這是最有效的避免並發問題的一種方式,如下圖所示
使用獨立狀態讓我們的設計更加簡單,因為只有一個線程能夠訪問對象,即使交換對象,也是不可變的對象。
並發模型
並行 Worker
第一個並發模型是並行 worker 模型,客戶端會把任務交給 代理人(Delegator)
,然后由代理人把工作分配給不同的 工人(worker)
。如下圖所示
並行 worker 的核心思想是,它主要有兩個進程即代理人和工人,Delegator 負責接收來自客戶端的任務並把任務下發,交給具體的 Worker 進行處理,Worker 處理完成后把結果返回給 Delegator,在 Delegator 接收到 Worker 處理的結果后對其進行匯總,然后交給客戶端。
並行 Worker 模型是 Java 並發模型中非常常見的一種模型。許多 java.util.concurrent
包下的並發工具都使用了這種模型。
並行 Worker 的優點
並行 Worker 模型的一個非常明顯的特點就是很容易理解,為了提高系統的並行度你可以增加多個 Worker 完成任務。
並行 Worker 模型的另外一個好處就是,它會將一個任務拆分成多個小任務,並發執行,Delegator 在接受到 Worker 的處理結果后就會返回給 Client,整個 Worker -> Delegator -> Client 的過程是異步
的。
並行 Worker 的缺點
同樣的,並行 Worker 模式同樣會有一些隱藏的缺點
共享狀態會變得很復雜
實際的並行 Worker 要比我們圖中畫出的更復雜,主要是並行 Worker 通常會訪問內存或共享數據庫中的某些共享數據。
這些共享狀態可能會使用一些工作隊列來保存業務數據、數據緩存、數據庫的連接池等。在線程通信中,線程需要確保共享狀態是否能夠讓其他線程共享,而不是僅僅停留在 CPU 緩存中讓自己可用,當然這些都是程序員在設計時就需要考慮的問題。線程需要避免 競態條件
,死鎖
和許多其他共享狀態造成的並發問題。
多線程在訪問共享數據時,會丟失並發性,因為操作系統要保證只有一個線程能夠訪問數據,這會導致共享數據的爭用和搶占。未搶占到資源的線程會 阻塞
。
現代的非阻塞並發算法可以減少爭用提高性能,但是非阻塞算法比較難以實現。
可持久化的數據結構(Persistent data structures)
是另外一個選擇。可持久化的數據結構在修改后始終會保留先前版本。因此,如果多個線程同時修改一個可持久化的數據結構,並且一個線程對其進行了修改,則修改的線程會獲得對新數據結構的引用。
雖然可持久化的數據結構是一個新的解決方法,但是這種方法實行起來卻有一些問題,比如,一個持久列表會將新元素添加到列表的開頭,並返回所添加的新元素的引用,但是其他線程仍然只持有列表中先前的第一個元素的引用,他們看不到新添加的元素。
持久化的數據結構比如 鏈表(LinkedList)
在硬件性能上表現不佳。列表中的每個元素都是一個對象,這些對象散布在計算機內存中。現代 CPU 的順序訪問往往要快的多,因此使用數組等順序訪問的數據結構則能夠獲得更高的性能。CPU 高速緩存可以將一個大的矩陣塊加載到高速緩存中,並讓 CPU 在加載后直接訪問 CPU 高速緩存中的數據。對於鏈表,將元素分散在整個 RAM 上,這實際上是不可能的。
無狀態的 worker
共享狀態可以由其他線程所修改,因此,worker 必須在每次操作共享狀態時重新讀取,以確保在副本上能夠正確工作。不在線程內部保持狀態的 worker 成為無狀態的 worker。
作業順序是不確定的
並行工作模型的另一個缺點是作業的順序不確定,無法保證首先執行或最后執行哪些作業。任務 A 在任務 B 之前分配給 worker,但是任務 B 可能在任務 A 之前執行。
流水線
第二種並發模型就是我們經常在生產車間遇到的 流水線並發模型
,下面是流水線設計模型的流程圖
這種組織架構就像是工廠中裝配線中的 worker,每個 worker 只完成全部工作的一部分,完成一部分后,worker 會將工作轉發給下一個 worker。
每道程序都在自己的線程中運行,彼此之間不會共享狀態,這種模型也被稱為無共享並發模型。
使用流水線並發模型通常被設計為非阻塞I/O
,也就是說,當沒有給 worker 分配任務時,worker 會做其他工作。非阻塞I/O 意味着當 worker 開始 I/O 操作,例如從網絡中讀取文件,worker 不會等待 I/O 調用完成。因為 I/O 操作很慢,所以等待 I/O 非常耗費時間。在等待 I/O 的同時,CPU 可以做其他事情,I/O 操作完成后的結果將傳遞給下一個 worker。下面是非阻塞 I/O 的流程圖
在實際情況中,任務通常不會按着一條裝配線流動,由於大多數程序需要做很多事情,因此需要根據完成的不同工作在不同的 worker 之間流動,如下圖所示
任務還可能需要多個 worker 共同參與完成
響應式 - 事件驅動系統
使用流水線模型的系統有時也被稱為 響應式
或者 事件驅動系統
,這種模型會根據外部的事件作出響應,事件可能是某個 HTTP 請求或者某個文件完成加載到內存中。
Actor 模型
在 Actor 模型中,每一個 Actor 其實就是一個 Worker, 每一個 Actor 都能夠處理任務。
簡單來說,Actor 模型是一個並發模型,它定義了一系列系統組件應該如何動作和交互的通用規則,最著名的使用這套規則的編程語言是 Erlang。一個參與者Actor
對接收到的消息做出響應,然后可以創建出更多的 Actor 或發送更多的消息,同時准備接收下一條消息。
Channels 模型
在 Channel 模型中,worker 通常不會直接通信,與此相對的,他們通常將事件發送到不同的 通道(Channel)
上,然后其他 worker 可以在這些通道上獲取消息,下面是 Channel 的模型圖
有的時候 worker 不需要明確知道接下來的 worker 是誰,他們只需要將作者寫入通道中,監聽 Channel 的 worker 可以訂閱或者取消訂閱,這種方式降低了 worker 和 worker 之間的耦合性。
流水線設計的優點
與並行設計模型相比,流水線模型具有一些優勢,具體優勢如下
不會存在共享狀態
因為流水線設計能夠保證 worker 在處理完成后再傳遞給下一個 worker,所以 worker 與 worker 之間不需要共享任何狀態,也就無需考慮並發問題。你甚至可以在實現上把每個 worker 看成是單線程的一種。
有狀態 worker
因為 worker 知道沒有其他線程修改自身的數據,所以流水線設計中的 worker 是有狀態的,有狀態的意思是他們可以將需要操作的數據保留在內存中,有狀態通常比無狀態更快。
更好的硬件整合
因為你可以把流水線看成是單線程的,而單線程的工作優勢在於它能夠和硬件的工作方式相同。因為有狀態的 worker 通常在 CPU 中緩存數據,這樣可以更快地訪問緩存的數據。
使任務更加有效的進行
可以對流水線並發模型中的任務進行排序,一般用來日志的寫入和恢復。
流水線設計的缺點
流水線並發模型的缺點是任務會涉及多個 worker,因此可能會分散在項目代碼的多個類中。因此很難確定每個 worker 都在執行哪個任務。流水線的代碼編寫也比較困難,設計許多嵌套回調處理程序的代碼通常被稱為 回調地獄
。回調地獄很難追蹤 debug。
函數性並行
函數性並行模型是最近才提出的一種並發模型,它的基本思路是使用函數調用來實現。消息的傳遞就相當於是函數的調用。傳遞給函數的參數都會被拷貝,因此在函數之外的任何實體都無法操縱函數內的數據。這使得函數執行類似於原子
操作。每個函數調用都可以獨立於任何其他函數調用執行。
當每個函數調用獨立執行時,每個函數都可以在單獨的 CPU 上執行。這也就是說,函數式並行並行相當於是各個 CPU 單獨執行各自的任務。
JDK 1.7 中的 ForkAndJoinPool
類就實現了函數性並行的功能。Java 8 提出了 stream 的概念,使用並行流也能夠實現大量集合的迭代。
函數性並行的難點是要知道函數的調用流程以及哪些 CPU 執行了哪些函數,跨 CPU 函數調用會帶來額外的開銷。
我們之前說過,線程就是進程中的一條順序流
,在 Java 中,每一條 Java 線程就像是 JVM 的一條順序流,就像是虛擬 CPU 一樣來執行代碼。Java 中的 main()
方法是一條特殊的線程,JVM 創建的 main 線程是一條主執行線程
,在 Java 中,方法都是由 main 方法發起的。在 main 方法中,你照樣可以創建其他的線程
(執行順序流),這些線程可以和 main 方法共同執行應用代碼。
Java 線程也是一種對象,它和其他對象一樣。Java 中的 Thread 表示線程,Thread 是 java.lang.Thread
類或其子類的實例。那么下面我們就來一起探討一下在 Java 中如何創建和啟動線程。
創建並啟動線程
在 Java 中,創建線程的方式主要有三種
- 通過繼承
Thread
類來創建線程 - 通過實現
Runnable
接口來創建線程 - 通過
Callable
和Future
來創建線程
下面我們分別探討一下這幾種創建方式
繼承 Thread 類來創建線程
第一種方式是繼承 Thread 類來創建線程,如下示例
public class TJavaThread extends Thread{ static int count; @Override public synchronized void run() { for(int i = 0;i < 10000;i++){ count++; } } public static void main(String[] args) throws InterruptedException { TJavaThread tJavaThread = new TJavaThread(); tJavaThread.start(); tJavaThread.join(); System.out.println("count = " + count); } }
線程的主要創建步驟如下
- 定義一個線程類使其繼承 Thread 類,並重寫其中的 run 方法,run 方法內部就是線程要完成的任務,因此 run 方法也被稱為
執行體
- 創建了 Thread 的子類,上面代碼中的子類是
TJavaThread
- 啟動方法需要注意,並不是直接調用
run
方法來啟動線程,而是使用start
方法來啟動線程。當然 run 方法可以調用,這樣的話就會變成普通方法調用,而不是新創建一個線程來調用了。
public static void main(String[] args) throws InterruptedException { TJavaThread tJavaThread = new TJavaThread(); tJavaThread.run(); System.out.println("count = " + count); }
這樣的話,整個 main 方法只有一條執行線程也就是 main 線程,由兩條執行線程變為一條執行線程
Thread 構造器只需要一個 Runnable 對象,調用 Thread 對象的 start() 方法為該線程執行必須的初始化操作,然后調用 Runnable 的 run 方法,以便在這個線程中啟動任務。我們上面使用了線程的 join
方法,它用來等待線程的執行結束,如果我們不加 join 方法,它就不會等待 tJavaThread 的執行完畢,輸出的結果可能就不是 10000
可以看到,在 run 方法還沒有結束前,run 就被返回了。也就是說,程序不會等到 run 方法執行完畢就會執行下面的指令。
使用繼承方式創建線程的優勢:編寫比較簡單;可以使用 this
關鍵字直接指向當前線程,而無需使用 Thread.currentThread()
來獲取當前線程。
使用繼承方式創建線程的劣勢:在 Java 中,只允許單繼承(拒絕肛精說使用內部類可以實現多繼承)的原則,所以使用繼承的方式,子類就不能再繼承其他類。
使用 Runnable 接口來創建線程
相對的,還可以使用 Runnable
接口來創建線程,如下示例
public class TJavaThreadUseImplements implements Runnable{ static int count; @Override public synchronized void run() { for(int i = 0;i < 10000;i++){ count++; } } public static void main(String[] args) throws InterruptedException { new Thread(new TJavaThreadUseImplements()).start(); System.out.println("count = " + count); } }
線程的主要創建步驟如下
- 首先定義 Runnable 接口,並重寫 Runnable 接口的 run 方法,run 方法的方法體同樣是該線程的線程執行體。
- 創建線程實例,可以使用上面代碼這種簡單的方式創建,也可以通過 new 出線程的實例來創建,如下所示
TJavaThreadUseImplements tJavaThreadUseImplements = new TJavaThreadUseImplements(); new Thread(tJavaThreadUseImplements).start();
- 再調用線程對象的 start 方法來啟動該線程。
線程在使用實現 Runnable
的同時也能實現其他接口,非常適合多個相同線程來處理同一份資源的情況,體現了面向對象的思想。
使用 Runnable 實現的劣勢是編程稍微繁瑣,如果要訪問當前線程,則必須使用 Thread.currentThread()
方法。
使用 Callable 接口來創建線程
Runnable 接口執行的是獨立的任務,Runnable 接口不會產生任何返回值,如果你希望在任務完成后能夠返回一個值的話,那么你可以實現 Callable
接口而不是 Runnable 接口。Java SE5 引入了 Callable 接口,它的示例如下
public class CallableTask implements Callable { static int count; public CallableTask(int count){ this.count = count; } @Override public Object call() { return count; } public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> task = new FutureTask((Callable<Integer>) () -> { for(int i = 0;i < 1000;i++){ count++; } return count; }); Thread thread = new Thread(task); thread.start(); Integer total = task.get(); System.out.println("total = " + total); } }
我想,使用 Callable 接口的好處你已經知道了吧,既能夠實現多個接口,也能夠得到執行結果的返回值。Callable 和 Runnable 接口還是有一些區別的,主要區別如下
- Callable 執行的任務有返回值,而 Runnable 執行的任務沒有返回值
- Callable(重寫)的方法是 call 方法,而 Runnable(重寫)的方法是 run 方法。
- call 方法可以拋出異常,而 Runnable 方法不能拋出異常
使用線程池來創建線程
首先先來認識一下頂級接口 Executor
,Executor 雖然不是傳統線程創建的方式之一,但是它卻成為了創建線程的替代者,使用線程池的好處如下
- 利用線程池能夠復用線程、控制最大並發數。
- 實現任務線程隊列
緩存策略
和拒絕機制
。 - 實現某些與時間相關的功能,如定時執行、周期執行等。
- 隔離線程環境。比如,交易服務和搜索服務在同一台服務器上,分別開啟兩個線程池,交易線程的資源消耗明顯要大;因此,通過配置獨立的線程池,將較慢的交易服務與搜索服務隔開,避免個服務線程互相影響。
你可以使用如下操作來替換線程創建
new Thread(new(RunnableTask())).start() // 替換為 Executor executor = new ExecutorSubClass() // 線程池實現類; executor.execute(new RunnableTask1()); executor.execute(new RunnableTask2());
ExecutorService
是 Executor 的默認實現,也是 Executor 的擴展接口,ThreadPoolExecutor 類提供了線程池的擴展實現。Executors
類為這些 Executor 提供了方便的工廠方法。下面是使用 ExecutorService 創建線程的幾種方式
CachedThreadPool
從而簡化了並發編程。Executor 在客戶端和任務之間提供了一個間接層;與客戶端直接執行任務不同,這個中介對象將執行任務。Executor 允許你管理異步
任務的執行,而無須顯示地管理線程的生命周期。
public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); for(int i = 0;i < 5;i++){ service.execute(new TestThread()); } service.shutdown(); }
CachedThreadPool
會為每個任務都創建一個線程。
注意:ExecutorService 對象是使用靜態的
Executors
創建的,這個方法可以確定 Executor 類型。對shutDown
的調用可以防止新任務提交給 ExecutorService ,這個線程在 Executor 中所有任務完成后退出。
FixedThreadPool
FixedThreadPool 使你可以使用有限
的線程集來啟動多線程
public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(5); for(int i = 0;i < 5;i++){ service.execute(new TestThread()); } service.shutdown(); }
有了 FixedThreadPool 使你可以一次性的預先執行高昂的線程分配,因此也就可以限制線程的數量。這可以節省時間,因為你不必為每個任務都固定的付出創建線程的開銷。
SingleThreadExecutor
SingleThreadExecutor 就是線程數量為 1
的 FixedThreadPool,如果向 SingleThreadPool 一次性提交了多個任務,那么這些任務將會排隊,每個任務都會在下一個任務開始前結束,所有的任務都將使用相同的線程。SingleThreadPool 會序列化所有提交給他的任務,並會維護它自己(隱藏)的懸掛隊列。
public static void main(String[] args) { ExecutorService service = Executors.newSingleThreadExecutor(); for(int i = 0;i < 5;i++){ service.execute(new TestThread()); } service.shutdown(); }
從輸出的結果就可以看到,任務都是挨着執行的。我為任務分配了五個線程,但是這五個線程不像是我們之前看到的有換進換出的效果,它每次都會先執行完自己的那個線程,然后余下的線程繼續走完
這條線程的執行路徑。你可以用 SingleThreadExecutor 來確保任意時刻都只有唯一一個任務在運行。
休眠
影響任務行為的一種簡單方式就是使線程 休眠,選定給定的休眠時間,調用它的 sleep()
方法, 一般使用的TimeUnit
這個時間類替換 Thread.sleep()
方法,示例如下:
public class SuperclassThread extends TestThread{ @Override public void run() { System.out.println(Thread.currentThread() + "starting ..." ); try { for(int i = 0;i < 5;i++){ if(i == 3){ System.out.println(Thread.currentThread() + "sleeping ..."); TimeUnit.MILLISECONDS.sleep(1000); } } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "wakeup and end ..."); } public static void main(String[] args) { ExecutorService executors = Executors.newCachedThreadPool(); for(int i = 0;i < 5;i++){ executors.execute(new SuperclassThread()); } executors.shutdown(); } }
關於 TimeUnit 中的 sleep() 方法和 Thread.sleep() 方法的比較,請參考下面這篇博客
優先級
上面提到線程調度器對每個線程的執行都是不可預知的,隨機執行的,那么有沒有辦法告訴線程調度器哪個任務想要優先被執行呢?你可以通過設置線程的優先級狀態,告訴線程調度器哪個線程的執行優先級比較高,請給這個騎手馬上派單,線程調度器傾向於讓優先級較高的線程優先執行,然而,這並不意味着優先級低的線程得不到執行,也就是說,優先級不會導致死鎖的問題。優先級較低的線程只是執行頻率較低。
public class SimplePriorities implements Runnable{ private int priority; public SimplePriorities(int priority) { this.priority = priority; } @Override public void run() { Thread.currentThread().setPriority(priority); for(int i = 0;i < 100;i++){ System.out.println(this); if(i % 10 == 0){ Thread.yield(); } } } @Override public String toString() { return Thread.currentThread() + " " + priority; } public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); for(int i = 0;i < 5;i++){ service.execute(new SimplePriorities(Thread.MAX_PRIORITY)); } service.execute(new SimplePriorities(Thread.MIN_PRIORITY)); } }
toString() 方法被覆蓋,以便通過使用 Thread.toString()
方法來打印線程的名稱。你可以改寫線程的默認輸出,這里采用了 Thread[pool-1-thread-1,10,main] 這種形式的輸出。
通過輸出,你可以看到,最后一個線程的優先級最低,其余的線程優先級最高。注意,優先級是在 run 開頭設置的,在構造器中設置它們不會有任何好處,因為這個時候線程還沒有執行任務。
盡管 JDK 有 10 個優先級,但是一般只有MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY 三種級別。
作出讓步
我們上面提過,如果知道一個線程已經在 run() 方法中運行的差不多了,那么它就可以給線程調度器一個提示:我已經完成了任務中最重要的部分,可以讓給別的線程使用 CPU 了。這個暗示將通過 yield() 方法作出。
有一個很重要的點就是,Thread.yield() 是建議執行切換CPU,而不是強制執行CPU切換。
對於任何重要的控制或者在調用應用時,都不能依賴於 yield()
方法,實際上, yield() 方法經常被濫用。
后台線程
后台(daemon)
線程,是指運行時在后台提供的一種服務線程,這種線程不是屬於必須的。當所有非后台線程結束時,程序也就停止了,同時會終止所有的后台線程。反過來說,只要有任何非后台線程還在運行,程序就不會終止。
public class SimpleDaemons implements Runnable{ @Override public void run() { while (true){ try { TimeUnit.MILLISECONDS.sleep(100); System.out.println(Thread.currentThread() + " " + this); } catch (InterruptedException e) { System.out.println("sleep() interrupted"); } } } public static void main(String[] args) throws InterruptedException { for(int i = 0;i < 10;i++){ Thread daemon = new Thread(new SimpleDaemons()); daemon.setDaemon(true); daemon.start(); } System.out.println("All Daemons started"); TimeUnit.MILLISECONDS.sleep(175); } }
在每次的循環中會創建 10 個線程,並把每個線程設置為后台線程,然后開始運行,for 循環會進行十次,然后輸出信息,隨后主線程睡眠一段時間后停止運行。在每次 run 循環中,都會打印當前線程的信息,主線程運行完畢,程序就執行完畢了。因為 daemon
是后台線程,無法影響主線程的執行。
但是當你把 daemon.setDaemon(true)
去掉時,while(true) 會進行無限循環,那么主線程一直在執行最重要的任務,所以會一直循環下去無法停止。
ThreadFactory
按需要創建線程的對象。使用線程工廠替換了 Thread 或者 Runnable 接口的硬連接,使程序能夠使用特殊的線程子類,優先級等。一般的創建方式為
class SimpleThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { return new Thread(r); } }
Executors.defaultThreadFactory 方法提供了一個更有用的簡單實現,它在返回之前將創建的線程上下文設置為已知值
ThreadFactory
是一個接口,它只有一個方法就是創建線程的方法
public interface ThreadFactory { // 構建一個新的線程。實現類可能初始化優先級,名稱,后台線程狀態和 線程組等 Thread newThread(Runnable r); }
下面來看一個 ThreadFactory 的例子
public class DaemonThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); return t; } } public class DaemonFromFactory implements Runnable{ @Override public void run() { while (true){ try { TimeUnit.MILLISECONDS.sleep(100); System.out.println(Thread.currentThread() + " " + this); } catch (InterruptedException e) { System.out.println("Interrupted"); } } } public static void main(String[] args) throws InterruptedException { ExecutorService service = Executors.newCachedThreadPool(new DaemonThreadFactory()); for(int i = 0;i < 10;i++){ service.execute(new DaemonFromFactory()); } System.out.println("All daemons started"); TimeUnit.MILLISECONDS.sleep(500); } }
Executors.newCachedThreadPool
可以接受一個線程池對象,創建一個根據需要創建新線程的線程池,但會在它們可用時重用先前構造的線程,並在需要時使用提供的 ThreadFactory 創建新線程。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }
加入一個線程
一個線程可以在其他線程上調用 join()
方法,其效果是等待一段時間直到第二個線程結束才正常執行。如果某個線程在另一個線程 t 上調用 t.join() 方法,此線程將被掛起,直到目標線程 t 結束才回復(可以用 t.isAlive() 返回為真假判斷)。
也可以在調用 join 時帶上一個超時參數,來設置到期時間,時間到期,join方法自動返回。
對 join 的調用也可以被中斷,做法是在線程上調用 interrupted
方法,這時需要用到 try...catch 子句
public class TestJoinMethod extends Thread{ @Override public void run() { for(int i = 0;i < 5;i++){ try { TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { System.out.println("Interrupted sleep"); } System.out.println(Thread.currentThread() + " " + i); } } public static void main(String[] args) throws InterruptedException { TestJoinMethod join1 = new TestJoinMethod(); TestJoinMethod join2 = new TestJoinMethod(); TestJoinMethod join3 = new TestJoinMethod(); join1.start(); // join1.join(); join2.start(); join3.start(); } }
join() 方法等待線程死亡。 換句話說,它會導致當前運行的線程停止執行,直到它加入的線程完成其任務。
線程異常捕獲
由於線程的本質,使你不能捕獲從線程中逃逸的異常,一旦異常逃出任務的 run 方法,它就會向外傳播到控制台,除非你采取特殊的步驟捕獲這種錯誤的異常,在 Java5 之前,你可以通過線程組來捕獲,但是在 Java 5 之后,就需要用 Executor 來解決問題,因為線程組不是一次好的嘗試。
下面的任務會在 run 方法的執行期間拋出一個異常,並且這個異常會拋到 run 方法的外面,而且 main 方法無法對它進行捕獲
public class ExceptionThread implements Runnable{ @Override public void run() { throw new RuntimeException(); } public static void main(String[] args) { try { ExecutorService service = Executors.newCachedThreadPool(); service.execute(new ExceptionThread()); }catch (Exception e){ System.out.println("eeeee"); } } }
為了解決這個問題,我們需要修改 Executor 產生線程的方式,Java5 提供了一個新的接口 Thread.UncaughtExceptionHandler
,它允許你在每個 Thread 上都附着一個異常處理器。Thread.UncaughtExceptionHandler.uncaughtException()
會在線程因未捕獲臨近死亡時被調用。
public class ExceptionThread2 implements Runnable{ @Override public void run() { Thread t = Thread.currentThread(); System.out.println("run() by " + t); System.out.println("eh = " + t.getUncaughtExceptionHandler()); // 手動拋出異常 throw new RuntimeException(); } } // 實現Thread.UncaughtExceptionHandler 接口,創建異常處理器 public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("caught " + e); } } public class HandlerThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { System.out.println(this + " creating new Thread"); Thread t = new Thread(r); System.out.println("created " + t); t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); System.out.println("ex = " + t.getUncaughtExceptionHandler()); return t; } } public class CaptureUncaughtException { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(new HandlerThreadFactory()); service.execute(new ExceptionThread2()); } }
在程序中添加了額外的追蹤機制,用來驗證工廠創建的線程會傳遞給UncaughtExceptionHandler
,你可以看到,未捕獲的異常是通過 uncaughtException
來捕獲的。
鳴謝:
https://www.cnblogs.com/cxuanBlog/p/13523033.html