操作系統(tǒng)的出現(xiàn)為我們的程序帶來了 并發(fā)性
愉耙,操作系統(tǒng)使我們的程序能夠同時(shí)運(yùn)行多個(gè)程序,一個(gè)程序就是一個(gè)進(jìn)程喻喳,也就相當(dāng)于同時(shí)運(yùn)行多個(gè)進(jìn)程邑遏。
操作系統(tǒng)是一個(gè)并發(fā)系統(tǒng)
,并發(fā)性是操作系統(tǒng)非常重要的特征恰矩,操作系統(tǒng)具有同時(shí)處理和調(diào)度多個(gè)程序的能力记盒,比如多個(gè) I/O 設(shè)備同時(shí)在輸入輸出;設(shè)備 I/O 和 CPU 計(jì)算同時(shí)進(jìn)行外傅;內(nèi)存中同時(shí)有多個(gè)系統(tǒng)和用戶程序被啟動(dòng)交替纪吮、穿插地執(zhí)行俩檬。操作系統(tǒng)在協(xié)調(diào)和分配進(jìn)程的同時(shí),操作系統(tǒng)也會(huì)為不同進(jìn)程分配不同的資源碾盟。
操作系統(tǒng)實(shí)現(xiàn)多個(gè)程序同時(shí)運(yùn)行解決了單個(gè)程序無法做到的問題棚辽,主要有下面三點(diǎn)
-
資源利用率
,我們上面說到冰肴,單個(gè)進(jìn)程存在資源浪費(fèi)的情況屈藐,舉個(gè)例子,當(dāng)你在為某個(gè)文件夾賦予權(quán)限的時(shí)候熙尉,輸入程序無法接受外部的輸入字符联逻,只有等到權(quán)限賦予完畢后才能接受外部輸入〖焯担總的來講包归,就是在等待程序時(shí)無法執(zhí)行其他工作。如果在等待程序時(shí)可以運(yùn)行另一個(gè)程序铅歼,那么將會(huì)大大提高資源的利用率公壤。(資源并不會(huì)覺得累)因?yàn)樗粫?huì)劃水~ -
公平性
,不同的用戶和程序都能夠使用計(jì)算機(jī)上的資源椎椰。一種高效的運(yùn)行方式是為不同的程序劃分時(shí)間片來使用資源骏融,但是有一點(diǎn)需要注意,操作系統(tǒng)可以決定不同進(jìn)程的優(yōu)先級(jí)翔烁。雖然每個(gè)進(jìn)程都有能夠公平享有資源的權(quán)利兼都,但是當(dāng)有一個(gè)進(jìn)程釋放資源后的同時(shí)有一個(gè)優(yōu)先級(jí)更高的進(jìn)程搶奪資源,就會(huì)造成優(yōu)先級(jí)低的進(jìn)程無法獲得資源套媚,進(jìn)而導(dǎo)致進(jìn)程饑餓缚态。 -
便利性
,單個(gè)進(jìn)程是是不用通信的堤瘤,通信的本質(zhì)就是信息交換
玫芦,及時(shí)進(jìn)行信息交換能夠避免信息孤島
,做重復(fù)性的工作本辐;任何并發(fā)能做的事情桥帆,單進(jìn)程也能夠?qū)崿F(xiàn),只不過這種方式效率很低慎皱,它是一種順序性
的老虫。
但是,順序編程(也稱為串行編程
)也不是一無是處
的茫多,串行編程的優(yōu)勢(shì)在于其直觀性和簡(jiǎn)單性祈匙,客觀來講,串行編程更適合我們?nèi)四X的思考方式,但是我們并不會(huì)滿足于順序編程夺欲,we want it more!!! 跪帝。資源利用率、公平性和便利性促使著進(jìn)程出現(xiàn)的同時(shí)些阅,也促使著線程
的出現(xiàn)伞剑。
如果你還不是很理解進(jìn)程和線程的區(qū)別的話,那么我就以我多年操作系統(tǒng)的經(jīng)驗(yàn)(吹牛逼市埋,實(shí)則半年)來為你解釋一下:進(jìn)程是一個(gè)應(yīng)用程序黎泣,而線程是應(yīng)用程序中的一條順序流。
進(jìn)程中會(huì)有多個(gè)線程來完成一些任務(wù)腰素,這些任務(wù)有可能相同有可能不同聘裁。每個(gè)線程都有自己的執(zhí)行順序。
每個(gè)線程都有自己的椆В空間衡便,這是線程私有的,還有一些其他線程內(nèi)部的和線程共享的資源洋访,如下所示镣陕。
在計(jì)算機(jī)中,一般堆棧指的就是棧姻政,而堆指的才是堆
線程會(huì)共享進(jìn)程范圍內(nèi)的資源呆抑,例如內(nèi)存和文件句柄,但是每個(gè)線程也有自己私有的內(nèi)容汁展,比如程序計(jì)數(shù)器鹊碍、棧以及局部變量。下面匯總了進(jìn)程和線程共享資源的區(qū)別
線程是一種輕量級(jí)
的進(jìn)程食绿,輕量級(jí)體現(xiàn)在線程的創(chuàng)建和銷毀要比進(jìn)程的開銷小很多侈咕。
注意:任何比較都是相對(duì)的。
在大多數(shù)現(xiàn)代操作系統(tǒng)中器紧,都以線程為基本的調(diào)度單位耀销,所以我們的視角著重放在對(duì)線程
的探究。
線程
什么是多線程
多線程意味著你能夠在同一個(gè)應(yīng)用程序中運(yùn)行多個(gè)線程铲汪,我們知道熊尉,指令是在 CPU 中執(zhí)行的,多線程應(yīng)用程序就像是具有多個(gè) CPU 在同時(shí)執(zhí)行應(yīng)用程序的代碼掌腰。
其實(shí)這是一種假象狰住,線程數(shù)量并不等于 CPU 數(shù)量,單個(gè) CPU 將在多個(gè)線程之間共享 CPU 的時(shí)間片齿梁,在給定的時(shí)間片內(nèi)執(zhí)行每個(gè)線程之間的切換催植,每個(gè)線程也可以由不同的 CPU 執(zhí)行,如下圖所示
并發(fā)和并行的關(guān)系
并發(fā)
意味著應(yīng)用程序會(huì)執(zhí)行多個(gè)的任務(wù),但是如果計(jì)算機(jī)只有一個(gè) CPU 的話查邢,那么應(yīng)用程序無法同時(shí)執(zhí)行多個(gè)的任務(wù),但是應(yīng)用程序又需要執(zhí)行多個(gè)任務(wù)酵幕,所以計(jì)算機(jī)在開始執(zhí)行下一個(gè)任務(wù)之前扰藕,它并沒有完成當(dāng)前的任務(wù),只是把狀態(tài)暫存芳撒,進(jìn)行任務(wù)切換邓深,CPU 在多個(gè)任務(wù)之間進(jìn)行切換,直到任務(wù)完成笔刹。如下圖所示
并行
是指應(yīng)用程序?qū)⑵淙蝿?wù)分解為較小的子任務(wù)芥备,這些子任務(wù)可以并行處理,例如在多個(gè)CPU上同時(shí)進(jìn)行舌菜。
優(yōu)勢(shì)和劣勢(shì)
合理使用線程是一門藝術(shù)萌壳,合理編寫一道準(zhǔn)確無誤的多線程程序更是一門藝術(shù),如果線程使用得當(dāng)日月,能夠有效的降低程序的開發(fā)和維護(hù)成本袱瓮。
Java 很好的在用戶空間實(shí)現(xiàn)了開發(fā)工具包,并在內(nèi)核空間提供系統(tǒng)調(diào)用來支持多線程編程爱咬,Java 支持了豐富的類庫 java.util.concurrent
和跨平臺(tái)的內(nèi)存模型
尺借,同時(shí)也提高了開發(fā)人員的門檻,并發(fā)一直以來是一個(gè)高階的主題精拟,但是現(xiàn)在燎斩,并發(fā)也成為了主流開發(fā)人員的必備素質(zhì)。
雖然線程帶來的好處很多蜂绎,但是編寫正確的多線程(并發(fā))程序是一件極困難的事情栅表,并發(fā)程序的 Bug 往往會(huì)詭異地出現(xiàn)又詭異的消失,在當(dāng)你認(rèn)為沒有問題的時(shí)候它就出現(xiàn)了荡碾,難以定位
是并發(fā)程序的一個(gè)特征谨读,所以在此基礎(chǔ)上你需要有扎實(shí)的并發(fā)基本功。那么坛吁,并發(fā)為什么會(huì)出現(xiàn)呢劳殖?
并發(fā)為什么會(huì)出現(xiàn)
計(jì)算機(jī)世界的快速發(fā)展離不開 CPU、內(nèi)存和 I/O 設(shè)備的高速發(fā)展拨脉,但是這三者一直存在速度差異性問題哆姻,我們可以從存儲(chǔ)器的層次結(jié)構(gòu)可以看出
CPU 內(nèi)部是寄存器的構(gòu)造,寄存器的訪問速度要高于高速緩存
玫膀,高速緩存的訪問速度要高于內(nèi)存矛缨,最慢的是磁盤訪問。
程序是在內(nèi)存中執(zhí)行的,程序里大部分語句都要訪問內(nèi)存箕昭,有些還需要訪問 I/O 設(shè)備灵妨,根據(jù)漏桶理論來說,程序整體的性能取決于最慢的操作也就是磁盤訪問速度落竹。
因?yàn)?CPU 速度太快了泌霍,所以為了發(fā)揮 CPU 的速度優(yōu)勢(shì),平衡這三者的速度差異述召,計(jì)算機(jī)體系機(jī)構(gòu)朱转、操作系統(tǒng)、編譯程序都做出了貢獻(xiàn)积暖,主要體現(xiàn)為:
- CPU 使用緩存來中和和內(nèi)存的訪問速度差異
- 操作系統(tǒng)提供進(jìn)程和線程調(diào)度藤为,讓 CPU 在執(zhí)行指令的同時(shí)分時(shí)復(fù)用線程,讓內(nèi)存和磁盤不斷交互夺刑,不同的
CPU 時(shí)間片
能夠執(zhí)行不同的任務(wù)缅疟,從而均衡這三者的差異 - 編譯程序提供優(yōu)化指令的執(zhí)行順序,讓緩存能夠合理的使用
我們?cè)谙硎苓@些便利的同時(shí)遍愿,多線程也為我們帶來了挑戰(zhàn)窿吩,下面我們就來探討一下并發(fā)問題為什么會(huì)出現(xiàn)以及多線程的源頭是什么
線程帶來的安全性問題
線程安全性是非常復(fù)雜的,在沒有采用同步機(jī)制
的情況下错览,多個(gè)線程中的執(zhí)行操作往往是不可預(yù)測(cè)的纫雁,這也是多線程帶來的挑戰(zhàn)之一,下面我們給出一段代碼倾哺,來看看安全性問題體現(xiàn)在哪
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);
}
}
這段程序輸出后會(huì)發(fā)現(xiàn)轧邪,i 的值每次都不一樣,這不符合我們的預(yù)測(cè)羞海,那么為什么會(huì)出現(xiàn)這種情況呢忌愚?我們先來分析一下程序的運(yùn)行過程。
TSynchronized
實(shí)現(xiàn)了 Runnable 接口却邓,并定義了一個(gè)靜態(tài)變量 i
硕糊,然后在 increase
方法中每次都增加 i 的值,在其實(shí)現(xiàn)的 run 方法中進(jìn)行循環(huán)調(diào)用腊徙,共執(zhí)行 1000 次简十。
可見性問題
在單核 CPU 時(shí)代,所有的線程共用一個(gè) CPU撬腾,CPU 緩存和內(nèi)存的一致性問題容易解決螟蝙,CPU 和 內(nèi)存之間
如果用圖來表示的話我想會(huì)是下面這樣
在多核時(shí)代,因?yàn)橛卸嗪说拇嬖诿裆担總€(gè)核都能夠獨(dú)立的運(yùn)行一個(gè)線程胰默,每顆 CPU 都有自己的緩存场斑,這時(shí) CPU 緩存與內(nèi)存的數(shù)據(jù)一致性就沒那么容易解決了,當(dāng)多個(gè)線程在不同的 CPU 上執(zhí)行時(shí)牵署,這些線程操作的是不同的 CPU 緩存
因?yàn)?i 是靜態(tài)變量漏隐,沒有經(jīng)過任何線程安全措施的保護(hù),多個(gè)線程會(huì)并發(fā)修改 i 的值奴迅,所以我們認(rèn)為 i 不是線程安全的锁保,導(dǎo)致這種結(jié)果的出現(xiàn)是由于 aThread 和 bThread 中讀取的 i 值彼此不可見,所以這是由于 可見性
導(dǎo)致的線程安全問題半沽。
原子性問題
看起來很普通的一段程序卻因?yàn)閮蓚€(gè)線程 aThread
和 bThread
交替執(zhí)行產(chǎn)生了不同的結(jié)果。但是根源不是因?yàn)閯?chuàng)建了兩個(gè)線程導(dǎo)致的吴菠,多線程只是產(chǎn)生線程安全性的必要條件者填,最終的根源出現(xiàn)在 i++
這個(gè)操作上。
這個(gè)操作怎么了做葵?這不就是一個(gè)給 i 遞增的操作嗎占哟?也就是 i++ => i = i + 1,這怎么就會(huì)產(chǎn)生問題了酿矢?
因?yàn)?i++
不是一個(gè) 原子性
操作榨乎,仔細(xì)想一下,i++ 其實(shí)有三個(gè)步驟瘫筐,讀取 i 的值蜜暑,執(zhí)行 i + 1 操作,然后把 i + 1 得出的值重新賦給 i(將結(jié)果寫入內(nèi)存)策肝。
當(dāng)兩個(gè)線程開始運(yùn)行后肛捍,每個(gè)線程都會(huì)把 i 的值讀入到 CPU 緩存中,然后執(zhí)行 + 1 操作之众,再把 + 1 之后的值寫入內(nèi)存拙毫。因?yàn)榫€程間都有各自的虛擬機(jī)棧和程序計(jì)數(shù)器,他們彼此之間沒有數(shù)據(jù)交換棺禾,所以當(dāng) aThread 執(zhí)行 + 1 操作后缀蹄,會(huì)把數(shù)據(jù)寫入到內(nèi)存,同時(shí) bThread 執(zhí)行 + 1 操作后膘婶,也會(huì)把數(shù)據(jù)寫入到內(nèi)存缺前,因?yàn)?CPU 時(shí)間片的執(zhí)行周期是不確定的,所以會(huì)出現(xiàn)當(dāng) aThread 還沒有把數(shù)據(jù)寫入內(nèi)存時(shí)悬襟,bThread 就會(huì)讀取內(nèi)存中的數(shù)據(jù)诡延,然后執(zhí)行 + 1操作,再寫回內(nèi)存古胆,從而覆蓋 i 的值肆良,導(dǎo)致 aThread 所做的努力白費(fèi)筛璧。
為什么上面的線程切換會(huì)出現(xiàn)問題呢?
我們先來考慮一下正常情況下(即不會(huì)出現(xiàn)線程安全性問題的情況下)兩條線程的執(zhí)行順序
可以看到惹恃,當(dāng) aThread 在執(zhí)行完整個(gè) i++ 的操作后夭谤,操作系統(tǒng)對(duì)線程進(jìn)行切換,由 aThread -> bThread巫糙,這是最理想的操作朗儒,一旦操作系統(tǒng)在任意 讀取/增加/寫入
階段產(chǎn)生線程切換,都會(huì)產(chǎn)生線程安全問題参淹。例如如下圖所示
最開始的時(shí)候醉锄,內(nèi)存中 i = 0,aThread 讀取內(nèi)存中的值并把它讀取到自己的寄存器中浙值,執(zhí)行 +1 操作恳不,此時(shí)發(fā)生線程切換,bThread 開始執(zhí)行开呐,讀取內(nèi)存中的值并把它讀取到自己的寄存器中烟勋,此時(shí)發(fā)生線程切換,線程切換至 aThread 開始運(yùn)行筐付,aThread 把自己寄存器的值寫回到內(nèi)存中卵惦,此時(shí)又發(fā)生線程切換,由 aThread -> bThread瓦戚,線程 bThread 把自己寄存器的值 +1 然后寫回內(nèi)存沮尿,寫完后內(nèi)存中的值不是 2 ,而是 1较解, 內(nèi)存中的 i 值被覆蓋了蛹找。
我們上面提到 原子性
這個(gè)概念,那么什么是原子性呢哨坪?
并發(fā)編程的原子性操作是完全獨(dú)立于任何其他進(jìn)程運(yùn)行的操作庸疾,原子操作多用于現(xiàn)代操作系統(tǒng)和并行處理系統(tǒng)中。
原子操作通常在內(nèi)核中使用当编,因?yàn)閮?nèi)核是操作系統(tǒng)的主要組件届慈。但是,大多數(shù)計(jì)算機(jī)硬件忿偷,編譯器和庫也提供原子性操作金顿。
在加載和存儲(chǔ)中,計(jì)算機(jī)硬件對(duì)存儲(chǔ)器字進(jìn)行讀取和寫入。為了對(duì)值進(jìn)行匹配、增加或者減小操作芽淡,一般通過原子操作進(jìn)行。在原子操作期間嫂拴,處理器可以在同一數(shù)據(jù)傳輸期間完成讀取和寫入播揪。 這樣,其他輸入/輸出機(jī)制或處理器無法執(zhí)行存儲(chǔ)器讀取或?qū)懭肴蝿?wù)筒狠,直到原子操作完成為止猪狈。
簡(jiǎn)單來講,就是原子操作要么全部執(zhí)行辩恼,要么全部不執(zhí)行雇庙。數(shù)據(jù)庫事務(wù)的原子性也是基于這個(gè)概念演進(jìn)的。
有序性問題
在并發(fā)編程中還有帶來讓人非常頭疼的 有序性
問題灶伊,有序性顧名思義就是順序性疆前,在計(jì)算機(jī)中指的就是指令的先后執(zhí)行順序。一個(gè)非常顯而易見的例子就是 JVM 中的類加載
這是一個(gè) JVM 加載類的過程圖聘萨,也稱為類的生命周期竹椒,類從加載到 JVM 到卸載一共會(huì)經(jīng)歷五個(gè)階段 加載、連接匈挖、初始化、使用康愤、卸載儡循。這五個(gè)過程的執(zhí)行順序是一定的,但是在連接階段征冷,也會(huì)分為三個(gè)過程择膝,即 驗(yàn)證、準(zhǔn)備检激、解析 階段肴捉,這三個(gè)階段的執(zhí)行順序不是確定的,通常交叉進(jìn)行叔收,在一個(gè)階段的執(zhí)行過程中會(huì)激活另一個(gè)階段齿穗。
有序性問題一般是編譯器帶來的,編譯器有的時(shí)候確實(shí)是 好心辦壞事饺律,它為了優(yōu)化系統(tǒng)性能窃页,往往更換指令的執(zhí)行順序。
活躍性問題
多線程還會(huì)帶來活躍性
問題复濒,如何定義活躍性問題呢脖卖?活躍性問題關(guān)注的是 某件事情是否會(huì)發(fā)生。
如果一組線程中的每個(gè)線程都在等待一個(gè)事件的發(fā)生巧颈,而這個(gè)事件只能由該組中正在等待的線程觸發(fā)畦木,這種情況會(huì)導(dǎo)致死鎖。
簡(jiǎn)單一點(diǎn)來表述一下砸泛,就是每個(gè)線程都在等待其他線程釋放資源十籍,而其他資源也在等待每個(gè)線程釋放資源蛆封,這樣沒有線程搶先釋放自己的資源,這種情況會(huì)產(chǎn)生死鎖妓雾,所有線程都會(huì)無限的等待下去娶吞。
死鎖的必要條件
造成死鎖的原因有四個(gè),破壞其中一個(gè)即可破壞死鎖
- 互斥條件:指進(jìn)程對(duì)所分配到的資源進(jìn)行排它性使用械姻,即在一段時(shí)間內(nèi)某資源只由一個(gè)進(jìn)程占用妒蛇。如果此時(shí)還有其它進(jìn)程請(qǐng)求資源,則請(qǐng)求者只能等待楷拳,直至占有資源的進(jìn)程釋放绣夺。
- 請(qǐng)求和保持條件:指進(jìn)程已經(jīng)保持至少一個(gè)資源,但又提出了新的資源請(qǐng)求欢揖,而該資源已被其它進(jìn)程占有陶耍,此時(shí)請(qǐng)求進(jìn)程阻塞,但又對(duì)自己已獲得的其它資源保持占有她混。
- 不剝奪條件:指進(jìn)程已獲得的資源烈钞,在未使用完之前,不能被剝奪坤按,只能在使用完時(shí)由自己釋放毯欣。
- 循環(huán)等待:指在發(fā)生死鎖時(shí),必然存在一個(gè)進(jìn)程對(duì)應(yīng)的環(huán)形鏈臭脓。
換句話說酗钞,死鎖線程集合中的每個(gè)線程都在等待另一個(gè)死鎖線程占有的資源。但是由于所有線程都不能運(yùn)行来累,它們之中任何一個(gè)資源都無法釋放資源砚作,所以沒有一個(gè)線程可以被喚醒。
如果說死鎖很癡情
的話嘹锁,那么活鎖
用一則成語來表示就是 弄巧成拙
葫录。
某些情況下,當(dāng)線程意識(shí)到它不能獲取所需要的下一個(gè)鎖時(shí)领猾,就會(huì)嘗試禮貌的釋放已經(jīng)獲得的鎖压昼,然后等待非常短的時(shí)間再次嘗試獲取×鲈耍可以想像一下這個(gè)場(chǎng)景:當(dāng)兩個(gè)人在狹路相逢的時(shí)候窍霞,都想給對(duì)方讓路,相同的步調(diào)會(huì)導(dǎo)致雙方都無法前進(jìn)拯坟。
現(xiàn)在假想有一對(duì)并行的線程用到了兩個(gè)資源但金。它們分別嘗試獲取另一個(gè)鎖失敗后,兩個(gè)線程都會(huì)釋放自己持有的鎖郁季,再次進(jìn)行嘗試冷溃,這個(gè)過程會(huì)一直進(jìn)行重復(fù)钱磅。很明顯,這個(gè)過程中沒有線程阻塞似枕,但是線程仍然不會(huì)向下執(zhí)行盖淡,這種狀況我們稱之為 活鎖(livelock)
。
如果我們期望的事情一直不會(huì)發(fā)生凿歼,就會(huì)產(chǎn)生活躍性問題褪迟,比如單線程中的無限循環(huán)
while(true){...}
for(;;){}
在多線程中,比如 aThread 和 bThread 都需要某種資源答憔,aThread 一直占用資源不釋放味赃,bThread 一直得不到執(zhí)行,就會(huì)造成活躍性問題虐拓,bThread 線程會(huì)產(chǎn)生饑餓
心俗,我們后面會(huì)說。
性能問題
與活躍性問題密切相關(guān)的是 性能
問題蓉驹,如果說活躍性問題關(guān)注的是最終的結(jié)果城榛,那么性能問題關(guān)注的就是造成結(jié)果的過程,性能問題有很多方面:比如服務(wù)時(shí)間過長(zhǎng)态兴,吞吐率過低狠持,資源消耗過高,在多線程中這樣的問題同樣存在诗茎。
在多線程中工坊,有一個(gè)非常重要的性能因素那就是我們上面提到的 線程切換
献汗,也稱為 上下文切換(Context Switch)
敢订,這種操作開銷很大。
在計(jì)算機(jī)世界中罢吃,老外都喜歡用 context 上下文這個(gè)詞楚午,這個(gè)詞涵蓋的內(nèi)容很多,包括上下文切換的資源尿招,寄存器的狀態(tài)矾柜、程序計(jì)數(shù)器等。context switch 一般指的就是這些上下文切換的資源就谜、寄存器狀態(tài)怪蔑、程序計(jì)數(shù)器的變化等。
在上下文切換中丧荐,會(huì)保存和恢復(fù)上下文缆瓣,丟失局部性,把大量的時(shí)間消耗在線程切換上而不是線程運(yùn)行上虹统。
為什么線程切換會(huì)開銷如此之大呢弓坞?線程間的切換會(huì)涉及到以下幾個(gè)步驟
將 CPU 從一個(gè)線程切換到另一線程涉及掛起當(dāng)前線程隧甚,保存其狀態(tài),例如寄存器渡冻,然后恢復(fù)到要切換的線程的狀態(tài)戚扳,加載新的程序計(jì)數(shù)器,此時(shí)線程切換實(shí)際上就已經(jīng)完成了族吻;此時(shí)帽借,CPU 不在執(zhí)行線程切換代碼,進(jìn)而執(zhí)行新的和線程關(guān)聯(lián)的代碼呼奢。
引起線程切換的幾種方式
線程間的切換一般是操作系統(tǒng)層面需要考慮的問題宜雀,那么引起線程上下文切換有哪幾種方式呢?或者說線程切換有哪幾種誘因呢握础?主要有下面幾種引起上下文切換的方式
- 當(dāng)前正在執(zhí)行的任務(wù)完成辐董,系統(tǒng)的 CPU 正常調(diào)度下一個(gè)需要運(yùn)行的線程
- 當(dāng)前正在執(zhí)行的任務(wù)遇到 I/O 等阻塞操作,線程調(diào)度器掛起此任務(wù)禀综,繼續(xù)調(diào)度下一個(gè)任務(wù)简烘。
- 多個(gè)任務(wù)并發(fā)搶占鎖資源,當(dāng)前任務(wù)沒有獲得鎖資源定枷,被線程調(diào)度器掛起孤澎,繼續(xù)調(diào)度下一個(gè)任務(wù)。
- 用戶的代碼掛起當(dāng)前任務(wù)欠窒,比如線程執(zhí)行 sleep 方法覆旭,讓出CPU。
- 使用硬件中斷的方式引起上下文切換
線程安全性
在 Java 中岖妄,要實(shí)現(xiàn)線程安全性型将,必須要正確的使用線程和鎖,但是這些只是滿足線程安全的一種方式荐虐,要編寫正確無誤的線程安全的代碼七兜,其核心就是對(duì)狀態(tài)訪問操作進(jìn)行管理。最重要的就是最 共享(Shared)
的 和 可變(Mutable)
的狀態(tài)福扬。只有共享和可變的變量才會(huì)出現(xiàn)問題腕铸,私有變量不會(huì)出現(xiàn)問題,參考程序計(jì)數(shù)器
铛碑。
對(duì)象的狀態(tài)可以理解為存儲(chǔ)在實(shí)例變量或者靜態(tài)變量中的數(shù)據(jù)狠裹,共享意味著某個(gè)變量可以被多個(gè)線程同時(shí)訪問、可變意味著變量在生命周期內(nèi)會(huì)發(fā)生變化汽烦。一個(gè)變量是否是線程安全的涛菠,取決于它是否被多個(gè)線程訪問。要使變量能夠被安全訪問,必須通過同步機(jī)制來對(duì)變量進(jìn)行修飾碗暗。
如果不采用同步機(jī)制的話颈将,那么就要避免多線程對(duì)共享變量的訪問,主要有下面兩種方式
- 不要在多線程之間共享變量
- 將共享變量置為不可變的
我們說了這么多次線程安全性言疗,那么什么是線程安全性呢晴圾?
什么是線程安全性
多個(gè)線程可以同時(shí)安全調(diào)用的代碼稱為線程安全的,如果一段代碼是安全的噪奄,那么這段代碼就不存在 競(jìng)態(tài)條件
死姚。僅僅當(dāng)多個(gè)線程共享資源時(shí),才會(huì)出現(xiàn)競(jìng)態(tài)條件勤篮。
根據(jù)上面的探討都毒,我們可以得出一個(gè)簡(jiǎn)單的結(jié)論:當(dāng)多個(gè)線程訪問某個(gè)類時(shí),這個(gè)類始終都能表現(xiàn)出正確的行為碰缔,那么就稱這個(gè)類是線程安全的账劲。
單線程就是一個(gè)線程數(shù)量為 1 的多線程,單線程一定是線程安全的金抡。讀取某個(gè)變量的值不會(huì)產(chǎn)生安全性問題瀑焦,因?yàn)椴还茏x取多少次,這個(gè)變量的值都不會(huì)被修改梗肝。
原子性
我們上面提到了原子性的概念榛瓮,你可以把原子性
操作想象成為一個(gè)不可分割
的整體,它的結(jié)果只有兩種巫击,要么全部執(zhí)行禀晓,要么全部回滾。你可以把原子性認(rèn)為是 婚姻關(guān)系
的一種坝锰,男人和女人只會(huì)產(chǎn)生兩種結(jié)果粹懒,好好的
和 說散就散
,一般男人的一生都可以把他看成是原子性的一種什黑,當(dāng)然我們不排除時(shí)間管理(線程切換)
的個(gè)例崎淳,我們知道線程切換必然會(huì)伴隨著安全性問題堪夭,男人要出去浪也會(huì)造成兩種結(jié)果愕把,這兩種結(jié)果分別對(duì)應(yīng)安全性的兩個(gè)結(jié)果:線程安全(好好的)和線程不安全(說散就散)。
競(jìng)態(tài)條件
有了上面的線程切換的功底森爽,那么競(jìng)態(tài)條件也就好定義了恨豁,它指的就是兩個(gè)或多個(gè)線程同時(shí)對(duì)一共享數(shù)據(jù)進(jìn)行修改,從而影響程序運(yùn)行的正確性時(shí)爬迟,這種就被稱為競(jìng)態(tài)條件(race condition) 橘蜜,線程切換是導(dǎo)致競(jìng)態(tài)條件出現(xiàn)的誘導(dǎo)因素,我們通過一個(gè)示例來說明,來看一段代碼
public class RaceCondition {
private Signleton single = null;
public Signleton newSingleton(){
if(single == null){
single = new Signleton();
}
return single;
}
}
在上面的代碼中计福,涉及到一個(gè)競(jìng)態(tài)條件跌捆,那就是判斷 single
的時(shí)候,如果 single 判斷為空象颖,此時(shí)發(fā)生了線程切換佩厚,另外一個(gè)線程執(zhí)行,判斷 single 的時(shí)候说订,也是空抄瓦,執(zhí)行 new 操作,然后線程切換回之前的線程陶冷,再執(zhí)行 new 操作钙姊,那么內(nèi)存中就會(huì)有兩個(gè) Singleton 對(duì)象。
加鎖機(jī)制
在 Java 中埂伦,有很多種方式來對(duì)共享和可變的資源進(jìn)行加鎖和保護(hù)煞额。Java 提供一種內(nèi)置的機(jī)制對(duì)資源進(jìn)行保護(hù):synchronized
關(guān)鍵字,它有三種保護(hù)機(jī)制
- 對(duì)方法進(jìn)行加鎖沾谜,確保多個(gè)線程中只有一個(gè)線程執(zhí)行方法立镶;
- 對(duì)某個(gè)對(duì)象實(shí)例(在我們上面的探討中,變量可以使用對(duì)象來替換)進(jìn)行加鎖类早,確保多個(gè)線程中只有一個(gè)線程對(duì)對(duì)象實(shí)例進(jìn)行訪問媚媒;
- 對(duì)類對(duì)象進(jìn)行加鎖,確保多個(gè)線程只有一個(gè)線程能夠訪問類中的資源涩僻。
synchronized 關(guān)鍵字對(duì)資源進(jìn)行保護(hù)的代碼塊俗稱 同步代碼塊(Synchronized Block)
缭召,例如
synchronized(lock){
// 線程安全的代碼
}
每個(gè) Java 對(duì)象都可以用做一個(gè)實(shí)現(xiàn)同步的鎖,這些鎖被稱為 內(nèi)置鎖(Instrinsic Lock)
或者 監(jiān)視器鎖(Monitor Lock)
逆日。線程在進(jìn)入同步代碼之前會(huì)自動(dòng)獲得鎖嵌巷,并且在退出同步代碼時(shí)自動(dòng)釋放鎖,而無論是通過正常執(zhí)行路徑退出還是通過異常路徑退出室抽,獲得內(nèi)置鎖的唯一途徑就是進(jìn)入這個(gè)由鎖保護(hù)的同步代碼塊或方法饭庞。
synchronized 的另一種隱含的語義就是 互斥
炫贤,互斥意味著獨(dú)占
,最多只有一個(gè)線程持有鎖,當(dāng)線程 A 嘗試獲得一個(gè)由線程 B 持有的鎖時(shí)符相,線程 A 必須等待或者阻塞佃却,直到線程 B 釋放這個(gè)鎖搀突,如果線程 B 不釋放鎖的話乏屯,那么線程 A 將會(huì)一直等待下去。
線程 A 獲得線程 B 持有的鎖時(shí)病梢,線程 A 必須等待或者阻塞胃珍,但是獲取鎖的線程 B 可以重入,重入的意思可以用一段代碼表示
public class Retreent {
public synchronized void doSomething(){
doSomethingElse();
System.out.println("doSomething......");
}
public synchronized void doSomethingElse(){
System.out.println("doSomethingElse......");
}
獲取 doSomething() 方法鎖的線程可以執(zhí)行 doSomethingElse() 方法,執(zhí)行完畢后可以重新執(zhí)行 doSomething() 方法中的內(nèi)容觅彰。鎖重入也支持子類和父類之間的重入吩蔑,具體的我們后面會(huì)進(jìn)行介紹。
volatile
是一種輕量級(jí)的 synchronized
填抬,也就是一種輕量級(jí)的加鎖方式哥纫,volatile 通過保證共享變量的可見性來從側(cè)面對(duì)對(duì)象進(jìn)行加鎖〕兆啵可見性的意思就是當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí)蛀骇,另外一個(gè)線程能夠 看見
這個(gè)修改的值。volatile 的執(zhí)行成本要比 synchronized
低很多读拆,因?yàn)?volatile 不會(huì)引起線程的上下文切換擅憔。
我們還可以使用原子類
來保證線程安全,原子類其實(shí)就是 rt.jar
下面以 atomic
開頭的類
除此之外檐晕,我們還可以使用 java.util.concurrent
工具包下的線程安全的集合類來確保線程安全暑诸,具體的實(shí)現(xiàn)類和其原理我們后面會(huì)說。
可以使用不同的并發(fā)模型來實(shí)現(xiàn)并發(fā)系統(tǒng)辟灰,并發(fā)模型說的是系統(tǒng)中的線程如何協(xié)作完成并發(fā)任務(wù)个榕。不同的并發(fā)模型以不同的方式拆分任務(wù),線程可以以不同的方式進(jìn)行通信和協(xié)作芥喇。
競(jìng)態(tài)條件和關(guān)鍵區(qū)域
競(jìng)態(tài)條件是在關(guān)鍵代碼區(qū)域發(fā)生的一種特殊條件西采。關(guān)鍵區(qū)域是由多個(gè)線程同時(shí)執(zhí)行的代碼部分,關(guān)鍵區(qū)域中的代碼執(zhí)行順序會(huì)對(duì)造成不一樣的結(jié)果继控。如果多個(gè)線程執(zhí)行一段關(guān)鍵代碼械馆,而這段關(guān)鍵代碼會(huì)因?yàn)閳?zhí)行順序不同而造成不同的結(jié)果時(shí),那么這段代碼就會(huì)包含競(jìng)爭(zhēng)條件武通。
并發(fā)模型和分布式系統(tǒng)很相似
并發(fā)模型其實(shí)和分布式系統(tǒng)模型非常相似霹崎,在并發(fā)模型中是線程
彼此進(jìn)行通信,而在分布式系統(tǒng)模型中是 進(jìn)程
彼此進(jìn)行通信冶忱。然而本質(zhì)上尾菇,進(jìn)程和線程也非常相似。這也就是為什么并發(fā)模型和分布式模型非常相似的原因囚枪。
分布式系統(tǒng)通常要比并發(fā)系統(tǒng)面臨更多的挑戰(zhàn)和問題比如進(jìn)程通信派诬、網(wǎng)絡(luò)可能出現(xiàn)異常,或者遠(yuǎn)程機(jī)器掛掉等等眶拉。但是一個(gè)并發(fā)模型同樣面臨著比如 CPU 故障千埃、網(wǎng)卡出現(xiàn)問題憔儿、硬盤出現(xiàn)問題等忆植。
因?yàn)椴l(fā)模型和分布式模型很相似,因此他們可以相互借鑒,例如用于線程分配的模型就類似于分布式系統(tǒng)環(huán)境中的負(fù)載均衡模型朝刊。
其實(shí)說白了耀里,分布式模型的思想就是借鑒并發(fā)模型的基礎(chǔ)上推演發(fā)展來的。
認(rèn)識(shí)兩個(gè)狀態(tài)
并發(fā)模型的一個(gè)重要的方面是拾氓,線程是否應(yīng)該共享狀態(tài)
冯挎,是具有共享狀態(tài)
還是獨(dú)立狀態(tài)
。共享狀態(tài)也就意味著在不同線程之間共享某些狀態(tài)
狀態(tài)其實(shí)就是數(shù)據(jù)
咙鞍,比如一個(gè)或者多個(gè)對(duì)象房官。當(dāng)線程要共享數(shù)據(jù)時(shí),就會(huì)造成 競(jìng)態(tài)條件
或者 死鎖
等問題续滋。當(dāng)然翰守,這些問題只是可能會(huì)出現(xiàn),具體實(shí)現(xiàn)方式取決于你是否安全的使用和訪問共享對(duì)象疲酌。
獨(dú)立的狀態(tài)表明狀態(tài)不會(huì)在多個(gè)線程之間共享蜡峰,如果線程之間需要通信的話,他們可以訪問不可變的對(duì)象來實(shí)現(xiàn)朗恳,這是最有效的避免并發(fā)問題的一種方式湿颅,如下圖所示
使用獨(dú)立狀態(tài)讓我們的設(shè)計(jì)更加簡(jiǎn)單,因?yàn)橹挥幸粋€(gè)線程能夠訪問對(duì)象粥诫,即使交換對(duì)象油航,也是不可變的對(duì)象。
并發(fā)模型
并行 Worker
第一個(gè)并發(fā)模型是并行 worker 模型怀浆,客戶端會(huì)把任務(wù)交給 代理人(Delegator)
劝堪,然后由代理人把工作分配給不同的 工人(worker)
。如下圖所示
并行 worker 的核心思想是揉稚,它主要有兩個(gè)進(jìn)程即代理人和工人秒啦,Delegator 負(fù)責(zé)接收來自客戶端的任務(wù)并把任務(wù)下發(fā),交給具體的 Worker 進(jìn)行處理搀玖,Worker 處理完成后把結(jié)果返回給 Delegator余境,在 Delegator 接收到 Worker 處理的結(jié)果后對(duì)其進(jìn)行匯總,然后交給客戶端灌诅。
并行 Worker 模型是 Java 并發(fā)模型中非常常見的一種模型芳来。許多 java.util.concurrent
包下的并發(fā)工具都使用了這種模型。
并行 Worker 的優(yōu)點(diǎn)
并行 Worker 模型的一個(gè)非常明顯的特點(diǎn)就是很容易理解猜拾,為了提高系統(tǒng)的并行度你可以增加多個(gè) Worker 完成任務(wù)即舌。
并行 Worker 模型的另外一個(gè)好處就是,它會(huì)將一個(gè)任務(wù)拆分成多個(gè)小任務(wù)挎袜,并發(fā)執(zhí)行顽聂,Delegator 在接受到 Worker 的處理結(jié)果后就會(huì)返回給 Client肥惭,整個(gè) Worker -> Delegator -> Client 的過程是異步
的。
并行 Worker 的缺點(diǎn)
同樣的紊搪,并行 Worker 模式同樣會(huì)有一些隱藏的缺點(diǎn)
共享狀態(tài)會(huì)變得很復(fù)雜
實(shí)際的并行 Worker 要比我們圖中畫出的更復(fù)雜蜜葱,主要是并行 Worker 通常會(huì)訪問內(nèi)存或共享數(shù)據(jù)庫中的某些共享數(shù)據(jù)。
這些共享狀態(tài)可能會(huì)使用一些工作隊(duì)列來保存業(yè)務(wù)數(shù)據(jù)耀石、數(shù)據(jù)緩存牵囤、數(shù)據(jù)庫的連接池等。在線程通信中滞伟,線程需要確保共享狀態(tài)是否能夠讓其他線程共享揭鳞,而不是僅僅停留在 CPU 緩存中讓自己可用,當(dāng)然這些都是程序員在設(shè)計(jì)時(shí)就需要考慮的問題梆奈。線程需要避免 競(jìng)態(tài)條件
汹桦,死鎖
和許多其他共享狀態(tài)造成的并發(fā)問題。
多線程在訪問共享數(shù)據(jù)時(shí)鉴裹,會(huì)丟失并發(fā)性舞骆,因?yàn)椴僮飨到y(tǒng)要保證只有一個(gè)線程能夠訪問數(shù)據(jù),這會(huì)導(dǎo)致共享數(shù)據(jù)的爭(zhēng)用和搶占径荔。未搶占到資源的線程會(huì) 阻塞
督禽。
現(xiàn)代的非阻塞并發(fā)算法可以減少爭(zhēng)用提高性能,但是非阻塞算法比較難以實(shí)現(xiàn)总处。
可持久化的數(shù)據(jù)結(jié)構(gòu)(Persistent data structures)
是另外一個(gè)選擇狈惫。可持久化的數(shù)據(jù)結(jié)構(gòu)在修改后始終會(huì)保留先前版本鹦马。因此胧谈,如果多個(gè)線程同時(shí)修改一個(gè)可持久化的數(shù)據(jù)結(jié)構(gòu),并且一個(gè)線程對(duì)其進(jìn)行了修改荸频,則修改的線程會(huì)獲得對(duì)新數(shù)據(jù)結(jié)構(gòu)的引用菱肖。
雖然可持久化的數(shù)據(jù)結(jié)構(gòu)是一個(gè)新的解決方法,但是這種方法實(shí)行起來卻有一些問題旭从,比如稳强,一個(gè)持久列表會(huì)將新元素添加到列表的開頭,并返回所添加的新元素的引用和悦,但是其他線程仍然只持有列表中先前的第一個(gè)元素的引用退疫,他們看不到新添加的元素。
持久化的數(shù)據(jù)結(jié)構(gòu)比如 鏈表(LinkedList)
在硬件性能上表現(xiàn)不佳鸽素。列表中的每個(gè)元素都是一個(gè)對(duì)象褒繁,這些對(duì)象散布在計(jì)算機(jī)內(nèi)存中。現(xiàn)代 CPU 的順序訪問往往要快的多馍忽,因此使用數(shù)組等順序訪問的數(shù)據(jù)結(jié)構(gòu)則能夠獲得更高的性能棒坏。CPU 高速緩存可以將一個(gè)大的矩陣塊加載到高速緩存中燕差,并讓 CPU 在加載后直接訪問 CPU 高速緩存中的數(shù)據(jù)。對(duì)于鏈表俊抵,將元素分散在整個(gè) RAM 上谁不,這實(shí)際上是不可能的坐梯。
無狀態(tài)的 worker
共享狀態(tài)可以由其他線程所修改徽诲,因此,worker 必須在每次操作共享狀態(tài)時(shí)重新讀取吵血,以確保在副本上能夠正確工作谎替。不在線程內(nèi)部保持狀態(tài)的 worker 成為無狀態(tài)的 worker。
作業(yè)順序是不確定的
并行工作模型的另一個(gè)缺點(diǎn)是作業(yè)的順序不確定蹋辅,無法保證首先執(zhí)行或最后執(zhí)行哪些作業(yè)钱贯。任務(wù) A 在任務(wù) B 之前分配給 worker,但是任務(wù) B 可能在任務(wù) A 之前執(zhí)行侦另。
流水線
第二種并發(fā)模型就是我們經(jīng)常在生產(chǎn)車間遇到的 流水線并發(fā)模型
秩命,下面是流水線設(shè)計(jì)模型的流程圖
這種組織架構(gòu)就像是工廠中裝配線中的 worker,每個(gè) worker 只完成全部工作的一部分褒傅,完成一部分后弃锐,worker 會(huì)將工作轉(zhuǎn)發(fā)給下一個(gè) worker。
每道程序都在自己的線程中運(yùn)行殿托,彼此之間不會(huì)共享狀態(tài)霹菊,這種模型也被稱為無共享并發(fā)模型。
使用流水線并發(fā)模型通常被設(shè)計(jì)為非阻塞I/O
支竹,也就是說旋廷,當(dāng)沒有給 worker 分配任務(wù)時(shí),worker 會(huì)做其他工作礼搁。非阻塞I/O 意味著當(dāng) worker 開始 I/O 操作饶碘,例如從網(wǎng)絡(luò)中讀取文件,worker 不會(huì)等待 I/O 調(diào)用完成馒吴。因?yàn)?I/O 操作很慢熊镣,所以等待 I/O 非常耗費(fèi)時(shí)間。在等待 I/O 的同時(shí)募书,CPU 可以做其他事情绪囱,I/O 操作完成后的結(jié)果將傳遞給下一個(gè) worker。下面是非阻塞 I/O 的流程圖
在實(shí)際情況中莹捡,任務(wù)通常不會(huì)按著一條裝配線流動(dòng)鬼吵,由于大多數(shù)程序需要做很多事情,因此需要根據(jù)完成的不同工作在不同的 worker 之間流動(dòng)篮赢,如下圖所示
任務(wù)還可能需要多個(gè) worker 共同參與完成
響應(yīng)式 - 事件驅(qū)動(dòng)系統(tǒng)
使用流水線模型的系統(tǒng)有時(shí)也被稱為 響應(yīng)式
或者 事件驅(qū)動(dòng)系統(tǒng)
齿椅,這種模型會(huì)根據(jù)外部的事件作出響應(yīng)琉挖,事件可能是某個(gè) HTTP 請(qǐng)求或者某個(gè)文件完成加載到內(nèi)存中。
Actor 模型
在 Actor 模型中涣脚,每一個(gè) Actor 其實(shí)就是一個(gè) Worker示辈, 每一個(gè) Actor 都能夠處理任務(wù)。
簡(jiǎn)單來說遣蚀,Actor 模型是一個(gè)并發(fā)模型矾麻,它定義了一系列系統(tǒng)組件應(yīng)該如何動(dòng)作和交互的通用規(guī)則,最著名的使用這套規(guī)則的編程語言是 Erlang芭梯。一個(gè)參與者Actor
對(duì)接收到的消息做出響應(yīng)险耀,然后可以創(chuàng)建出更多的 Actor 或發(fā)送更多的消息,同時(shí)準(zhǔn)備接收下一條消息玖喘。
Channels 模型
在 Channel 模型中甩牺,worker 通常不會(huì)直接通信,與此相對(duì)的累奈,他們通常將事件發(fā)送到不同的 通道(Channel)
上贬派,然后其他 worker 可以在這些通道上獲取消息,下面是 Channel 的模型圖
有的時(shí)候 worker 不需要明確知道接下來的 worker 是誰澎媒,他們只需要將作者寫入通道中搞乏,監(jiān)聽 Channel 的 worker 可以訂閱或者取消訂閱,這種方式降低了 worker 和 worker 之間的耦合性旱幼。
流水線設(shè)計(jì)的優(yōu)點(diǎn)
與并行設(shè)計(jì)模型相比查描,流水線模型具有一些優(yōu)勢(shì),具體優(yōu)勢(shì)如下
不會(huì)存在共享狀態(tài)
因?yàn)榱魉€設(shè)計(jì)能夠保證 worker 在處理完成后再傳遞給下一個(gè) worker柏卤,所以 worker 與 worker 之間不需要共享任何狀態(tài)冬三,也就無需考慮并發(fā)問題。你甚至可以在實(shí)現(xiàn)上把每個(gè) worker 看成是單線程的一種缘缚。
有狀態(tài) worker
因?yàn)?worker 知道沒有其他線程修改自身的數(shù)據(jù)勾笆,所以流水線設(shè)計(jì)中的 worker 是有狀態(tài)的,有狀態(tài)的意思是他們可以將需要操作的數(shù)據(jù)保留在內(nèi)存中桥滨,有狀態(tài)通常比無狀態(tài)更快窝爪。
更好的硬件整合
因?yàn)槟憧梢园蚜魉€看成是單線程的,而單線程的工作優(yōu)勢(shì)在于它能夠和硬件的工作方式相同齐媒。因?yàn)橛袪顟B(tài)的 worker 通常在 CPU 中緩存數(shù)據(jù)蒲每,這樣可以更快地訪問緩存的數(shù)據(jù)。
使任務(wù)更加有效的進(jìn)行
可以對(duì)流水線并發(fā)模型中的任務(wù)進(jìn)行排序喻括,一般用來日志的寫入和恢復(fù)邀杏。
流水線設(shè)計(jì)的缺點(diǎn)
流水線并發(fā)模型的缺點(diǎn)是任務(wù)會(huì)涉及多個(gè) worker,因此可能會(huì)分散在項(xiàng)目代碼的多個(gè)類中唬血。因此很難確定每個(gè) worker 都在執(zhí)行哪個(gè)任務(wù)望蜡。流水線的代碼編寫也比較困難唤崭,設(shè)計(jì)許多嵌套回調(diào)處理程序的代碼通常被稱為 回調(diào)地獄
〔甭桑回調(diào)地獄很難追蹤 debug谢肾。
函數(shù)性并行
函數(shù)性并行模型是最近才提出的一種并發(fā)模型,它的基本思路是使用函數(shù)調(diào)用來實(shí)現(xiàn)小泉。消息的傳遞就相當(dāng)于是函數(shù)的調(diào)用芦疏。傳遞給函數(shù)的參數(shù)都會(huì)被拷貝,因此在函數(shù)之外的任何實(shí)體都無法操縱函數(shù)內(nèi)的數(shù)據(jù)膏孟。這使得函數(shù)執(zhí)行類似于原子
操作眯分。每個(gè)函數(shù)調(diào)用都可以獨(dú)立于任何其他函數(shù)調(diào)用執(zhí)行拌汇。
當(dāng)每個(gè)函數(shù)調(diào)用獨(dú)立執(zhí)行時(shí)柒桑,每個(gè)函數(shù)都可以在單獨(dú)的 CPU 上執(zhí)行。這也就是說噪舀,函數(shù)式并行并行相當(dāng)于是各個(gè) CPU 單獨(dú)執(zhí)行各自的任務(wù)魁淳。
JDK 1.7 中的 ForkAndJoinPool
類就實(shí)現(xiàn)了函數(shù)性并行的功能。Java 8 提出了 stream 的概念与倡,使用并行流也能夠?qū)崿F(xiàn)大量集合的迭代界逛。
函數(shù)性并行的難點(diǎn)是要知道函數(shù)的調(diào)用流程以及哪些 CPU 執(zhí)行了哪些函數(shù),跨 CPU 函數(shù)調(diào)用會(huì)帶來額外的開銷纺座。
我們之前說過息拜,線程就是進(jìn)程中的一條順序流
,在 Java 中净响,每一條 Java 線程就像是 JVM 的一條順序流少欺,就像是虛擬 CPU 一樣來執(zhí)行代碼。Java 中的 main()
方法是一條特殊的線程馋贤,JVM 創(chuàng)建的 main 線程是一條主執(zhí)行線程
赞别,在 Java 中,方法都是由 main 方法發(fā)起的配乓。在 main 方法中仿滔,你照樣可以創(chuàng)建其他的線程
(執(zhí)行順序流),這些線程可以和 main 方法共同執(zhí)行應(yīng)用代碼犹芹。
Java 線程也是一種對(duì)象崎页,它和其他對(duì)象一樣。Java 中的 Thread 表示線程腰埂,Thread 是 java.lang.Thread
類或其子類的實(shí)例飒焦。那么下面我們就來一起探討一下在 Java 中如何創(chuàng)建和啟動(dòng)線程。
創(chuàng)建并啟動(dòng)線程
在 Java 中盐固,創(chuàng)建線程的方式主要有三種
- 通過繼承
Thread
類來創(chuàng)建線程 - 通過實(shí)現(xiàn)
Runnable
接口來創(chuàng)建線程 - 通過
Callable
和Future
來創(chuàng)建線程
下面我們分別探討一下這幾種創(chuàng)建方式
繼承 Thread 類來創(chuàng)建線程
第一種方式是繼承 Thread 類來創(chuàng)建線程荒给,如下示例
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);
}
}
線程的主要?jiǎng)?chuàng)建步驟如下
- 定義一個(gè)線程類使其繼承 Thread 類丈挟,并重寫其中的 run 方法,run 方法內(nèi)部就是線程要完成的任務(wù)志电,因此 run 方法也被稱為
執(zhí)行體
- 創(chuàng)建了 Thread 的子類曙咽,上面代碼中的子類是
TJavaThread
- 啟動(dòng)方法需要注意,并不是直接調(diào)用
run
方法來啟動(dòng)線程挑辆,而是使用start
方法來啟動(dòng)線程例朱。當(dāng)然 run 方法可以調(diào)用,這樣的話就會(huì)變成普通方法調(diào)用鱼蝉,而不是新創(chuàng)建一個(gè)線程來調(diào)用了洒嗤。
public static void main(String[] args) throws InterruptedException {
TJavaThread tJavaThread = new TJavaThread();
tJavaThread.run();
System.out.println("count = " + count);
}
這樣的話,整個(gè) main 方法只有一條執(zhí)行線程也就是 main 線程魁亦,由兩條執(zhí)行線程變?yōu)橐粭l執(zhí)行線程
Thread 構(gòu)造器只需要一個(gè) Runnable 對(duì)象渔隶,調(diào)用 Thread 對(duì)象的 start() 方法為該線程執(zhí)行必須的初始化操作,然后調(diào)用 Runnable 的 run 方法洁奈,以便在這個(gè)線程中啟動(dòng)任務(wù)间唉。我們上面使用了線程的 join
方法,它用來等待線程的執(zhí)行結(jié)束利术,如果我們不加 join 方法呈野,它就不會(huì)等待 tJavaThread 的執(zhí)行完畢,輸出的結(jié)果可能就不是 10000
可以看到印叁,在 run 方法還沒有結(jié)束前被冒,run 就被返回了。也就是說轮蜕,程序不會(huì)等到 run 方法執(zhí)行完畢就會(huì)執(zhí)行下面的指令昨悼。
使用繼承方式創(chuàng)建線程的優(yōu)勢(shì):編寫比較簡(jiǎn)單;可以使用 this
關(guān)鍵字直接指向當(dāng)前線程肠虽,而無需使用 Thread.currentThread()
來獲取當(dāng)前線程幔戏。
使用繼承方式創(chuàng)建線程的劣勢(shì):在 Java 中,只允許單繼承(拒絕肛精說使用內(nèi)部類可以實(shí)現(xiàn)多繼承)的原則税课,所以使用繼承的方式闲延,子類就不能再繼承其他類。
使用 Runnable 接口來創(chuàng)建線程
相對(duì)的韩玩,還可以使用 Runnable
接口來創(chuàng)建線程望伦,如下示例
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);
}
}
線程的主要?jiǎng)?chuàng)建步驟如下
- 首先定義 Runnable 接口围段,并重寫 Runnable 接口的 run 方法球及,run 方法的方法體同樣是該線程的線程執(zhí)行體照捡。
- 創(chuàng)建線程實(shí)例,可以使用上面代碼這種簡(jiǎn)單的方式創(chuàng)建,也可以通過 new 出線程的實(shí)例來創(chuàng)建佛析,如下所示
TJavaThreadUseImplements tJavaThreadUseImplements = new TJavaThreadUseImplements();
new Thread(tJavaThreadUseImplements).start();
- 再調(diào)用線程對(duì)象的 start 方法來啟動(dòng)該線程益老。
線程在使用實(shí)現(xiàn) Runnable
的同時(shí)也能實(shí)現(xiàn)其他接口,非常適合多個(gè)相同線程來處理同一份資源的情況寸莫,體現(xiàn)了面向?qū)ο蟮乃枷搿?/p>
使用 Runnable 實(shí)現(xiàn)的劣勢(shì)是編程稍微繁瑣捺萌,如果要訪問當(dāng)前線程,則必須使用 Thread.currentThread()
方法膘茎。
使用 Callable 接口來創(chuàng)建線程
Runnable 接口執(zhí)行的是獨(dú)立的任務(wù)桃纯,Runnable 接口不會(huì)產(chǎn)生任何返回值,如果你希望在任務(wù)完成后能夠返回一個(gè)值的話披坏,那么你可以實(shí)現(xiàn) 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 接口的好處你已經(jīng)知道了吧伞梯,既能夠?qū)崿F(xiàn)多個(gè)接口,也能夠得到執(zhí)行結(jié)果的返回值着茸。Callable 和 Runnable 接口還是有一些區(qū)別的壮锻,主要區(qū)別如下
- Callable 執(zhí)行的任務(wù)有返回值琐旁,而 Runnable 執(zhí)行的任務(wù)沒有返回值
- Callable(重寫)的方法是 call 方法涮阔,而 Runnable(重寫)的方法是 run 方法。
- call 方法可以拋出異常灰殴,而 Runnable 方法不能拋出異常
使用線程池來創(chuàng)建線程
首先先來認(rèn)識(shí)一下頂級(jí)接口 Executor
敬特,Executor 雖然不是傳統(tǒng)線程創(chuàng)建的方式之一,但是它卻成為了創(chuàng)建線程的替代者牺陶,使用線程池的好處如下
- 利用線程池能夠復(fù)用線程伟阔、控制最大并發(fā)數(shù)。
- 實(shí)現(xiàn)任務(wù)線程隊(duì)列
緩存策略
和拒絕機(jī)制
掰伸。 - 實(shí)現(xiàn)某些與時(shí)間相關(guān)的功能皱炉,如定時(shí)執(zhí)行、周期執(zhí)行等狮鸭。
- 隔離線程環(huán)境合搅。比如,交易服務(wù)和搜索服務(wù)在同一臺(tái)服務(wù)器上歧蕉,分別開啟兩個(gè)線程池灾部,交易線程的資源消耗明顯要大;因此惯退,通過配置獨(dú)立的線程池赌髓,將較慢的交易服務(wù)與搜索服務(wù)隔開,避免個(gè)服務(wù)線程互相影響。
你可以使用如下操作來替換線程創(chuàng)建
new Thread(new(RunnableTask())).start()
// 替換為
Executor executor = new ExecutorSubClass() // 線程池實(shí)現(xiàn)類;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
ExecutorService
是 Executor 的默認(rèn)實(shí)現(xiàn)锁蠕,也是 Executor 的擴(kuò)展接口夷野,ThreadPoolExecutor 類提供了線程池的擴(kuò)展實(shí)現(xiàn)。Executors
類為這些 Executor 提供了方便的工廠方法荣倾。下面是使用 ExecutorService 創(chuàng)建線程的幾種方式
CachedThreadPool
從而簡(jiǎn)化了并發(fā)編程扫责。Executor 在客戶端和任務(wù)之間提供了一個(gè)間接層;與客戶端直接執(zhí)行任務(wù)不同逃呼,這個(gè)中介對(duì)象將執(zhí)行任務(wù)鳖孤。Executor 允許你管理異步
任務(wù)的執(zhí)行,而無須顯示地管理線程的生命周期抡笼。
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
for(int i = 0;i < 5;i++){
service.execute(new TestThread());
}
service.shutdown();
}
CachedThreadPool
會(huì)為每個(gè)任務(wù)都創(chuàng)建一個(gè)線程苏揣。
注意:ExecutorService 對(duì)象是使用靜態(tài)的
Executors
創(chuàng)建的,這個(gè)方法可以確定 Executor 類型推姻。對(duì)shutDown
的調(diào)用可以防止新任務(wù)提交給 ExecutorService 平匈,這個(gè)線程在 Executor 中所有任務(wù)完成后退出。
FixedThreadPool
FixedThreadPool 使你可以使用有限
的線程集來啟動(dòng)多線程
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 使你可以一次性的預(yù)先執(zhí)行高昂的線程分配藏古,因此也就可以限制線程的數(shù)量增炭。這可以節(jié)省時(shí)間,因?yàn)槟悴槐貫槊總€(gè)任務(wù)都固定的付出創(chuàng)建線程的開銷拧晕。
SingleThreadExecutor
SingleThreadExecutor 就是線程數(shù)量為 1
的 FixedThreadPool隙姿,如果向 SingleThreadPool 一次性提交了多個(gè)任務(wù),那么這些任務(wù)將會(huì)排隊(duì)厂捞,每個(gè)任務(wù)都會(huì)在下一個(gè)任務(wù)開始前結(jié)束输玷,所有的任務(wù)都將使用相同的線程。SingleThreadPool 會(huì)序列化所有提交給他的任務(wù)靡馁,并會(huì)維護(hù)它自己(隱藏)的懸掛隊(duì)列欲鹏。
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
for(int i = 0;i < 5;i++){
service.execute(new TestThread());
}
service.shutdown();
}
從輸出的結(jié)果就可以看到,任務(wù)都是挨著執(zhí)行的臭墨。我為任務(wù)分配了五個(gè)線程赔嚎,但是這五個(gè)線程不像是我們之前看到的有換進(jìn)換出的效果,它每次都會(huì)先執(zhí)行完自己的那個(gè)線程胧弛,然后余下的線程繼續(xù)走完
這條線程的執(zhí)行路徑尤误。你可以用 SingleThreadExecutor 來確保任意時(shí)刻都只有唯一一個(gè)任務(wù)在運(yùn)行。
休眠
影響任務(wù)行為的一種簡(jiǎn)單方式就是使線程 休眠叶圃,選定給定的休眠時(shí)間袄膏,調(diào)用它的 sleep()
方法, 一般使用的TimeUnit
這個(gè)時(shí)間類替換 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();
}
}
關(guān)于 TimeUnit 中的 sleep() 方法和 Thread.sleep() 方法的比較沉馆,請(qǐng)參考下面這篇博客
優(yōu)先級(jí)
上面提到線程調(diào)度器對(duì)每個(gè)線程的執(zhí)行都是不可預(yù)知的码党,隨機(jī)執(zhí)行的,那么有沒有辦法告訴線程調(diào)度器哪個(gè)任務(wù)想要優(yōu)先被執(zhí)行呢斥黑?你可以通過設(shè)置線程的優(yōu)先級(jí)狀態(tài)揖盘,告訴線程調(diào)度器哪個(gè)線程的執(zhí)行優(yōu)先級(jí)比較高,請(qǐng)給這個(gè)騎手馬上派單锌奴,線程調(diào)度器傾向于讓優(yōu)先級(jí)較高的線程優(yōu)先執(zhí)行兽狭,然而,這并不意味著優(yōu)先級(jí)低的線程得不到執(zhí)行鹿蜀,也就是說箕慧,優(yōu)先級(jí)不會(huì)導(dǎo)致死鎖的問題。優(yōu)先級(jí)較低的線程只是執(zhí)行頻率較低茴恰。
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()
方法來打印線程的名稱。你可以改寫線程的默認(rèn)輸出往枣,這里采用了 Thread[pool-1-thread-1,10,main] 這種形式的輸出伐庭。
通過輸出,你可以看到分冈,最后一個(gè)線程的優(yōu)先級(jí)最低圾另,其余的線程優(yōu)先級(jí)最高。注意雕沉,優(yōu)先級(jí)是在 run 開頭設(shè)置的集乔,在構(gòu)造器中設(shè)置它們不會(huì)有任何好處,因?yàn)檫@個(gè)時(shí)候線程還沒有執(zhí)行任務(wù)蘑秽。
盡管 JDK 有 10 個(gè)優(yōu)先級(jí)饺著,但是一般只有MAX_PRIORITY,NORM_PRIORITY肠牲,MIN_PRIORITY 三種級(jí)別。
作出讓步
我們上面提過靴跛,如果知道一個(gè)線程已經(jīng)在 run() 方法中運(yùn)行的差不多了缀雳,那么它就可以給線程調(diào)度器一個(gè)提示:我已經(jīng)完成了任務(wù)中最重要的部分,可以讓給別的線程使用 CPU 了梢睛。這個(gè)暗示將通過 yield() 方法作出肥印。
有一個(gè)很重要的點(diǎn)就是,Thread.yield() 是建議執(zhí)行切換CPU绝葡,而不是強(qiáng)制執(zhí)行CPU切換深碱。
對(duì)于任何重要的控制或者在調(diào)用應(yīng)用時(shí),都不能依賴于 yield()
方法藏畅,實(shí)際上敷硅, yield() 方法經(jīng)常被濫用。
后臺(tái)線程
后臺(tái)(daemon)
線程,是指運(yùn)行時(shí)在后臺(tái)提供的一種服務(wù)線程绞蹦,這種線程不是屬于必須的力奋。當(dāng)所有非后臺(tái)線程結(jié)束時(shí),程序也就停止了幽七,同時(shí)會(huì)終止所有的后臺(tái)線程景殷。反過來說,只要有任何非后臺(tái)線程還在運(yùn)行澡屡,程序就不會(huì)終止猿挚。
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);
}
}
在每次的循環(huán)中會(huì)創(chuàng)建 10 個(gè)線程,并把每個(gè)線程設(shè)置為后臺(tái)線程驶鹉,然后開始運(yùn)行亭饵,for 循環(huán)會(huì)進(jìn)行十次,然后輸出信息梁厉,隨后主線程睡眠一段時(shí)間后停止運(yùn)行辜羊。在每次 run 循環(huán)中,都會(huì)打印當(dāng)前線程的信息词顾,主線程運(yùn)行完畢八秃,程序就執(zhí)行完畢了。因?yàn)?daemon
是后臺(tái)線程肉盹,無法影響主線程的執(zhí)行昔驱。
但是當(dāng)你把 daemon.setDaemon(true)
去掉時(shí),while(true) 會(huì)進(jìn)行無限循環(huán)上忍,那么主線程一直在執(zhí)行最重要的任務(wù)骤肛,所以會(huì)一直循環(huán)下去無法停止。
ThreadFactory
按需要?jiǎng)?chuàng)建線程的對(duì)象窍蓝。使用線程工廠替換了 Thread 或者 Runnable 接口的硬連接腋颠,使程序能夠使用特殊的線程子類,優(yōu)先級(jí)等吓笙。一般的創(chuàng)建方式為
class SimpleThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
return new Thread(r);
}
}
Executors.defaultThreadFactory 方法提供了一個(gè)更有用的簡(jiǎn)單實(shí)現(xiàn)淑玫,它在返回之前將創(chuàng)建的線程上下文設(shè)置為已知值
ThreadFactory
是一個(gè)接口,它只有一個(gè)方法就是創(chuàng)建線程的方法
public interface ThreadFactory {
// 構(gòu)建一個(gè)新的線程面睛。實(shí)現(xiàn)類可能初始化優(yōu)先級(jí)絮蒿,名稱,后臺(tái)線程狀態(tài)和 線程組等
Thread newThread(Runnable r);
}
下面來看一個(gè) 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
可以接受一個(gè)線程池對(duì)象叁鉴,創(chuàng)建一個(gè)根據(jù)需要?jiǎng)?chuàng)建新線程的線程池土涝,但會(huì)在它們可用時(shí)重用先前構(gòu)造的線程,并在需要時(shí)使用提供的 ThreadFactory 創(chuàng)建新線程幌墓。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
加入一個(gè)線程
一個(gè)線程可以在其他線程上調(diào)用 join()
方法但壮,其效果是等待一段時(shí)間直到第二個(gè)線程結(jié)束才正常執(zhí)行冀泻。如果某個(gè)線程在另一個(gè)線程 t 上調(diào)用 t.join() 方法,此線程將被掛起茵肃,直到目標(biāo)線程 t 結(jié)束才回復(fù)(可以用 t.isAlive() 返回為真假判斷)腔长。
也可以在調(diào)用 join 時(shí)帶上一個(gè)超時(shí)參數(shù),來設(shè)置到期時(shí)間验残,時(shí)間到期捞附,join方法自動(dòng)返回。
對(duì) join 的調(diào)用也可以被中斷您没,做法是在線程上調(diào)用 interrupted
方法鸟召,這時(shí)需要用到 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() 方法等待線程死亡。 換句話說氨鹏,它會(huì)導(dǎo)致當(dāng)前運(yùn)行的線程停止執(zhí)行欧募,直到它加入的線程完成其任務(wù)。
線程異常捕獲
由于線程的本質(zhì)仆抵,使你不能捕獲從線程中逃逸的異常跟继,一旦異常逃出任務(wù)的 run 方法,它就會(huì)向外傳播到控制臺(tái)镣丑,除非你采取特殊的步驟捕獲這種錯(cuò)誤的異常舔糖,在 Java5 之前,你可以通過線程組來捕獲莺匠,但是在 Java 5 之后金吗,就需要用 Executor 來解決問題,因?yàn)榫€程組不是一次好的嘗試趣竣。
下面的任務(wù)會(huì)在 run 方法的執(zhí)行期間拋出一個(gè)異常摇庙,并且這個(gè)異常會(huì)拋到 run 方法的外面,而且 main 方法無法對(duì)它進(jìn)行捕獲
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");
}
}
}
為了解決這個(gè)問題遥缕,我們需要修改 Executor 產(chǎn)生線程的方式卫袒,Java5 提供了一個(gè)新的接口 Thread.UncaughtExceptionHandler
,它允許你在每個(gè) Thread 上都附著一個(gè)異常處理器通砍。Thread.UncaughtExceptionHandler.uncaughtException()
會(huì)在線程因未捕獲臨近死亡時(shí)被調(diào)用玛臂。
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());
// 手動(dòng)拋出異常
throw new RuntimeException();
}
}
// 實(shí)現(xiàn)Thread.UncaughtExceptionHandler 接口,創(chuàng)建異常處理器
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());
}
}
在程序中添加了額外的追蹤機(jī)制封孙,用來驗(yàn)證工廠創(chuàng)建的線程會(huì)傳遞給UncaughtExceptionHandler
,你可以看到讽营,未捕獲的異常是通過 uncaughtException
來捕獲的虎忌。
你好,我是 cxuan橱鹏,我自己手寫了四本 PDF膜蠢,分別是 Java基礎(chǔ)總結(jié)堪藐、HTTP 核心總結(jié)、計(jì)算機(jī)基礎(chǔ)知識(shí)挑围,操作系統(tǒng)核心總結(jié)礁竞,我已經(jīng)整理成為 PDF,可以關(guān)注公眾號(hào) Java建設(shè)者 回復(fù) PDF 領(lǐng)取優(yōu)質(zhì)資料杉辙。