2w字 + 40張圖帶你參透并發(fā)編程!
點擊藍色“程序員cxuan?”關(guān)注我喲
加個“星標”,歡迎來撩


1
?并發(fā)歷史?
在計算機最早期的時候,沒有操作系統(tǒng),執(zhí)行程序只需要一種方式,那就是從頭到尾依次執(zhí)行。任何資源都會為這個程序服務(wù),在計算機使用某些資源時,其他資源就會空閑,就會存在 浪費資源 的情況。
?這里說的浪費資源指的是資源空閑,沒有充分使用的情況。
?
操作系統(tǒng)的出現(xiàn)為我們的程序帶來了 并發(fā)性,操作系統(tǒng)使我們的程序能夠同時運行多個程序,一個程序就是一個進程,也就相當于同時運行多個進程。
操作系統(tǒng)是一個并發(fā)系統(tǒng),并發(fā)性是操作系統(tǒng)非常重要的特征,操作系統(tǒng)具有同時處理和調(diào)度多個程序的能力,比如多個 I/O 設(shè)備同時在輸入輸出;設(shè)備 I/O 和 CPU 計算同時進行;內(nèi)存中同時有多個系統(tǒng)和用戶程序被啟動交替、穿插地執(zhí)行。操作系統(tǒng)在協(xié)調(diào)和分配進程的同時,操作系統(tǒng)也會為不同進程分配不同的資源。
操作系統(tǒng)實現(xiàn)多個程序同時運行解決了單個程序無法做到的問題,主要有下面三點
資源利用率,我們上面說到,單個進程存在資源浪費的情況,舉個例子,當你在為某個文件夾賦予權(quán)限的時候,輸入程序無法接受外部的輸入字符,只有等到權(quán)限賦予完畢后才能接受外部輸入。總的來講,就是在等待程序時無法執(zhí)行其他工作。如果在等待程序時可以運行另一個程序,那么將會大大提高資源的利用率。(資源并不會覺得累)因為它不會劃水~公平性,不同的用戶和程序都能夠使用計算機上的資源。一種高效的運行方式是為不同的程序劃分時間片來使用資源,但是有一點需要注意,操作系統(tǒng)可以決定不同進程的優(yōu)先級。雖然每個進程都有能夠公平享有資源的權(quán)利,但是當有一個進程釋放資源后的同時有一個優(yōu)先級更高的進程搶奪資源,就會造成優(yōu)先級低的進程無法獲得資源,進而導致進程饑餓。便利性,單個進程是是不用通信的,通信的本質(zhì)就是信息交換,及時進行信息交換能夠避免信息孤島,做重復性的工作;任何并發(fā)能做的事情,單進程也能夠?qū)崿F(xiàn),只不過這種方式效率很低,它是一種順序性的。
但是,順序編程(也稱為串行編程)也不是一無是處的,串行編程的優(yōu)勢在于其「直觀性和簡單性」,客觀來講,串行編程更適合我們?nèi)四X的思考方式,但是我們并不會滿足于順序編程,「we want it more!!!」 。資源利用率、公平性和便利性促使著進程出現(xiàn)的同時,也促使著線程的出現(xiàn)。
如果你還不是很理解進程和線程的區(qū)別的話,那么我就以我多年操作系統(tǒng)的經(jīng)驗(吹牛逼,實則半年)來為你解釋一下:「進程是一個應(yīng)用程序,而線程是應(yīng)用程序中的一條順序流」。

進程中會有多個線程來完成一些任務(wù),這些任務(wù)有可能相同有可能不同。每個線程都有自己的執(zhí)行順序。

每個線程都有自己的棧空間,這是線程私有的,還有一些其他線程內(nèi)部的和線程共享的資源,如下所示。
?在計算機中,一般堆棧指的就是棧,而堆指的才是堆
?
線程會共享進程范圍內(nèi)的資源,例如內(nèi)存和文件句柄,但是每個線程也有自己私有的內(nèi)容,比如程序計數(shù)器、棧以及局部變量。下面匯總了進程和線程共享資源的區(qū)別

線程是一種輕量級的進程,輕量級體現(xiàn)在線程的創(chuàng)建和銷毀要比進程的開銷小很多。
?注意:任何比較都是相對的。
?
在大多數(shù)現(xiàn)代操作系統(tǒng)中,都以線程為基本的調(diào)度單位,所以我們的視角著重放在對線程的探究。

2
?線程?
什么是多線程
多線程意味著你能夠在同一個應(yīng)用程序中運行多個線程,我們知道,指令是在 CPU 中執(zhí)行的,多線程應(yīng)用程序就像是具有多個 CPU 在同時執(zhí)行應(yīng)用程序的代碼。

其實這是一種假象,線程數(shù)量并不等于 CPU 數(shù)量,單個 CPU 將在多個線程之間共享 CPU 的時間片,在給定的時間片內(nèi)執(zhí)行每個線程之間的切換,每個線程也可以由不同的 CPU 執(zhí)行,如下圖所示

并發(fā)和并行的關(guān)系
并發(fā)意味著應(yīng)用程序會執(zhí)行多個的任務(wù),但是如果計算機只有一個 CPU 的話,那么應(yīng)用程序無法同時執(zhí)行多個的任務(wù),但是應(yīng)用程序又需要執(zhí)行多個任務(wù),所以計算機在開始執(zhí)行下一個任務(wù)之前,它并沒有完成當前的任務(wù),只是把狀態(tài)暫存,進行任務(wù)切換,CPU 在多個任務(wù)之間進行切換,直到任務(wù)完成。如下圖所示

并行是指應(yīng)用程序?qū)⑵淙蝿?wù)分解為較小的子任務(wù),這些子任務(wù)可以并行處理,例如在多個CPU上同時進行。

優(yōu)勢和劣勢
合理使用線程是一門藝術(shù),合理編寫一道準確無誤的多線程程序更是一門藝術(shù),如果線程使用得當,能夠有效的降低程序的開發(fā)和維護成本。
Java 很好的在用戶空間實現(xiàn)了開發(fā)工具包,并在內(nèi)核空間提供系統(tǒng)調(diào)用來支持多線程編程,Java 支持了豐富的類庫 java.util.concurrent 和跨平臺的內(nèi)存模型,同時也提高了開發(fā)人員的門檻,并發(fā)一直以來是一個高階的主題,但是現(xiàn)在,并發(fā)也成為了主流開發(fā)人員的必備素質(zhì)。
雖然線程帶來的好處很多,但是編寫正確的多線程(并發(fā))程序是一件極困難的事情,并發(fā)程序的 Bug 往往會詭異地出現(xiàn)又詭異的消失,在當你認為沒有問題的時候它就出現(xiàn)了,難以定位 是并發(fā)程序的一個特征,所以在此基礎(chǔ)上你需要有扎實的并發(fā)基本功。那么,并發(fā)為什么會出現(xiàn)呢?
并發(fā)為什么會出現(xiàn)
計算機世界的快速發(fā)展離不開 CPU、內(nèi)存和 I/O 設(shè)備的高速發(fā)展,但是這三者一直存在速度差異性問題,我們可以從存儲器的層次結(jié)構(gòu)可以看出

CPU 內(nèi)部是寄存器的構(gòu)造,寄存器的訪問速度要高于高速緩存,高速緩存的訪問速度要高于內(nèi)存,最慢的是磁盤訪問。
程序是在內(nèi)存中執(zhí)行的,程序里大部分語句都要訪問內(nèi)存,有些還需要訪問 I/O 設(shè)備,根據(jù)漏桶理論來說,程序整體的性能取決于最慢的操作也就是磁盤訪問速度。
因為 CPU 速度太快了,所以為了發(fā)揮 CPU 的速度優(yōu)勢,平衡這三者的速度差異,計算機體系機構(gòu)、操作系統(tǒng)、編譯程序都做出了貢獻,主要體現(xiàn)為:
CPU 使用緩存來中和和內(nèi)存的訪問速度差異 操作系統(tǒng)提供進程和線程調(diào)度,讓 CPU 在執(zhí)行指令的同時分時復用線程,讓內(nèi)存和磁盤不斷交互,不同的 CPU 時間片能夠執(zhí)行不同的任務(wù),從而均衡這三者的差異編譯程序提供優(yōu)化指令的執(zhí)行順序,讓緩存能夠合理的使用
我們在享受這些便利的同時,多線程也為我們帶來了挑戰(zhàn),下面我們就來探討一下并發(fā)問題為什么會出現(xiàn)以及多線程的源頭是什么
線程帶來的安全性問題
線程安全性是非常復雜的,在沒有采用同步機制的情況下,多個線程中的執(zhí)行操作往往是不可預測的,這也是多線程帶來的挑戰(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);
????}
}
這段程序輸出后會發(fā)現(xiàn),i 的值每次都不一樣,這不符合我們的預測,那么為什么會出現(xiàn)這種情況呢?我們先來分析一下程序的運行過程。
TSynchronized 實現(xiàn)了 Runnable 接口,并定義了一個靜態(tài)變量 i,然后在 increase 方法中每次都增加 i 的值,在其實現(xiàn)的 run 方法中進行循環(huán)調(diào)用,共執(zhí)行 1000 次。
可見性問題
在單核 CPU 時代,所有的線程共用一個 CPU,CPU 緩存和內(nèi)存的一致性問題容易解決,CPU 和 內(nèi)存之間
如果用圖來表示的話我想會是下面這樣

在多核時代,因為有多核的存在,每個核都能夠獨立的運行一個線程,每顆 CPU 都有自己的緩存,這時 CPU 緩存與內(nèi)存的數(shù)據(jù)一致性就沒那么容易解決了,當多個線程在不同的 CPU 上執(zhí)行時,這些線程操作的是不同的 CPU 緩存

因為 i 是靜態(tài)變量,沒有經(jīng)過任何線程安全措施的保護,多個線程會并發(fā)修改 i 的值,所以我們認為 i 不是線程安全的,導致這種結(jié)果的出現(xiàn)是由于 aThread 和 bThread 中讀取的 i 值彼此不可見,所以這是由于 可見性 導致的線程安全問題。
原子性問題
看起來很普通的一段程序卻因為兩個線程 aThread 和 bThread 交替執(zhí)行產(chǎn)生了不同的結(jié)果。但是根源不是因為創(chuàng)建了兩個線程導致的,多線程只是產(chǎn)生線程安全性的必要條件,最終的根源出現(xiàn)在 i++ 這個操作上。
這個操作怎么了?這不就是一個給 i 遞增的操作嗎?也就是 「i++ => i = i + 1」,這怎么就會產(chǎn)生問題了?
因為 i++ 不是一個 原子性 操作,仔細想一下,i++ 其實有三個步驟,讀取 i 的值,執(zhí)行 i + 1 操作,然后把 i + 1 得出的值重新賦給 i(將結(jié)果寫入內(nèi)存)。
當兩個線程開始運行后,每個線程都會把 i 的值讀入到 CPU 緩存中,然后執(zhí)行 + 1 操作,再把 + 1 之后的值寫入內(nèi)存。因為線程間都有各自的虛擬機棧和程序計數(shù)器,他們彼此之間沒有數(shù)據(jù)交換,所以當 aThread 執(zhí)行 + 1 操作后,會把數(shù)據(jù)寫入到內(nèi)存,同時 bThread 執(zhí)行 + 1 操作后,也會把數(shù)據(jù)寫入到內(nèi)存,因為 CPU 時間片的執(zhí)行周期是不確定的,所以會出現(xiàn)當 aThread 還沒有把數(shù)據(jù)寫入內(nèi)存時,bThread 就會讀取內(nèi)存中的數(shù)據(jù),然后執(zhí)行 + 1操作,再寫回內(nèi)存,從而覆蓋 i 的值,導致 aThread 所做的努力白費。

為什么上面的線程切換會出現(xiàn)問題呢?
我們先來考慮一下正常情況下(即不會出現(xiàn)線程安全性問題的情況下)兩條線程的執(zhí)行順序

可以看到,當 aThread 在執(zhí)行完整個 i++ 的操作后,操作系統(tǒng)對線程進行切換,由 aThread -> bThread,這是最理想的操作,一旦操作系統(tǒng)在任意 讀取/增加/寫入 階段產(chǎn)生線程切換,都會產(chǎn)生線程安全問題。例如如下圖所示

最開始的時候,內(nèi)存中 i = 0,aThread 讀取內(nèi)存中的值并把它讀取到自己的寄存器中,執(zhí)行 +1 操作,此時發(fā)生線程切換,bThread 開始執(zhí)行,讀取內(nèi)存中的值并把它讀取到自己的寄存器中,此時發(fā)生線程切換,線程切換至 aThread 開始運行,aThread 把自己寄存器的值寫回到內(nèi)存中,此時又發(fā)生線程切換,由 aThread -> bThread,線程 bThread 把自己寄存器的值 +1 然后寫回內(nèi)存,寫完后內(nèi)存中的值不是 2 ,而是 1, 內(nèi)存中的 i 值被覆蓋了。
我們上面提到 原子性 這個概念,那么什么是原子性呢?
?并發(fā)編程的原子性操作是完全獨立于任何其他進程運行的操作,原子操作多用于現(xiàn)代操作系統(tǒng)和并行處理系統(tǒng)中。
原子操作通常在內(nèi)核中使用,因為內(nèi)核是操作系統(tǒng)的主要組件。但是,大多數(shù)計算機硬件,編譯器和庫也提供原子性操作。
在加載和存儲中,計算機硬件對存儲器字進行讀取和寫入。為了對值進行匹配、增加或者減小操作,一般通過原子操作進行。在原子操作期間,處理器可以在同一數(shù)據(jù)傳輸期間完成讀取和寫入。這樣,其他輸入/輸出機制或處理器無法執(zhí)行存儲器讀取或?qū)懭肴蝿?wù),直到原子操作完成為止。
?
簡單來講,就是「原子操作要么全部執(zhí)行,要么全部不執(zhí)行」。數(shù)據(jù)庫事務(wù)的原子性也是基于這個概念演進的。
有序性問題
在并發(fā)編程中還有帶來讓人非常頭疼的 有序性 問題,有序性顧名思義就是順序性,在計算機中指的就是指令的先后執(zhí)行順序。一個非常顯而易見的例子就是 JVM 中的類加載

這是一個 JVM 加載類的過程圖,也稱為類的生命周期,類從加載到 JVM 到卸載一共會經(jīng)歷五個階段 「加載、連接、初始化、使用、卸載」。這五個過程的執(zhí)行順序是一定的,但是在連接階段,也會分為三個過程,即 「驗證、準備、解析」 階段,這三個階段的執(zhí)行順序不是確定的,通常交叉進行,在一個階段的執(zhí)行過程中會激活另一個階段。
有序性問題一般是編譯器帶來的,編譯器有的時候確實是 「好心辦壞事」,它為了優(yōu)化系統(tǒng)性能,往往更換指令的執(zhí)行順序。
活躍性問題
多線程還會帶來活躍性問題,如何定義活躍性問題呢?活躍性問題關(guān)注的是 「某件事情是否會發(fā)生」。
「如果一組線程中的每個線程都在等待一個事件的發(fā)生,而這個事件只能由該組中正在等待的線程觸發(fā),這種情況會導致死鎖」。
簡單一點來表述一下,就是每個線程都在等待其他線程釋放資源,而其他資源也在等待每個線程釋放資源,這樣沒有線程搶先釋放自己的資源,這種情況會產(chǎn)生死鎖,所有線程都會無限的等待下去。
「死鎖的必要條件」
造成死鎖的原因有四個,破壞其中一個即可破壞死鎖
互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內(nèi)某資源只由一個進程占用。如果此時還有其它進程請求資源,則請求者只能等待,直至占有資源的進程釋放。 請求和保持條件:指進程已經(jīng)保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程占有,此時請求進程阻塞,但又對自己已獲得的其它資源保持占有。 不剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。 循環(huán)等待:指在發(fā)生死鎖時,必然存在一個進程對應(yīng)的環(huán)形鏈。
換句話說,死鎖線程集合中的每個線程都在等待另一個死鎖線程占有的資源。但是由于所有線程都不能運行,它們之中任何一個資源都無法釋放資源,所以沒有一個線程可以被喚醒。
如果說死鎖很癡情的話,那么活鎖用一則成語來表示就是 弄巧成拙。
某些情況下,當線程意識到它不能獲取所需要的下一個鎖時,就會嘗試禮貌的釋放已經(jīng)獲得的鎖,然后等待非常短的時間再次嘗試獲取。可以想像一下這個場景:當兩個人在狹路相逢的時候,都想給對方讓路,相同的步調(diào)會導致雙方都無法前進。
現(xiàn)在假想有一對并行的線程用到了兩個資源。它們分別嘗試獲取另一個鎖失敗后,兩個線程都會釋放自己持有的鎖,再次進行嘗試,這個過程會一直進行重復。很明顯,這個過程中沒有線程阻塞,但是線程仍然不會向下執(zhí)行,這種狀況我們稱之為 活鎖(livelock)。
如果我們期望的事情一直不會發(fā)生,就會產(chǎn)生活躍性問題,比如單線程中的無限循環(huán)
while(true){...}
for(;;){}
在多線程中,比如 aThread 和 bThread 都需要某種資源,aThread 一直占用資源不釋放,bThread 一直得不到執(zhí)行,就會造成活躍性問題,bThread 線程會產(chǎn)生饑餓,我們后面會說。
性能問題
與活躍性問題密切相關(guān)的是 性能 問題,如果說活躍性問題關(guān)注的是最終的結(jié)果,那么性能問題關(guān)注的就是造成結(jié)果的過程,性能問題有很多方面:比如「服務(wù)時間過長,吞吐率過低,資源消耗過高」,在多線程中這樣的問題同樣存在。
在多線程中,有一個非常重要的性能因素那就是我們上面提到的 線程切換,也稱為 上下文切換(Context Switch),這種操作開銷很大。
?在計算機世界中,老外都喜歡用 context 上下文這個詞,這個詞涵蓋的內(nèi)容很多,包括上下文切換的資源,寄存器的狀態(tài)、程序計數(shù)器等。context switch 一般指的就是這些上下文切換的資源、寄存器狀態(tài)、程序計數(shù)器的變化等。
?
在上下文切換中,會保存和恢復上下文,丟失局部性,把大量的時間消耗在線程切換上而不是線程運行上。
為什么線程切換會開銷如此之大呢?線程間的切換會涉及到以下幾個步驟

將 CPU 從一個線程切換到另一線程涉及掛起當前線程,保存其狀態(tài),例如寄存器,然后恢復到要切換的線程的狀態(tài),加載新的程序計數(shù)器,此時線程切換實際上就已經(jīng)完成了;此時,CPU 不在執(zhí)行線程切換代碼,進而執(zhí)行新的和線程關(guān)聯(lián)的代碼。
引起線程切換的幾種方式
線程間的切換一般是操作系統(tǒng)層面需要考慮的問題,那么引起線程上下文切換有哪幾種方式呢?或者說線程切換有哪幾種誘因呢?主要有下面幾種引起上下文切換的方式
當前正在執(zhí)行的任務(wù)完成,系統(tǒng)的 CPU 正常調(diào)度下一個需要運行的線程 當前正在執(zhí)行的任務(wù)遇到 I/O 等阻塞操作,線程調(diào)度器掛起此任務(wù),繼續(xù)調(diào)度下一個任務(wù)。 多個任務(wù)并發(fā)搶占鎖資源,當前任務(wù)沒有獲得鎖資源,被線程調(diào)度器掛起,繼續(xù)調(diào)度下一個任務(wù)。 用戶的代碼掛起當前任務(wù),比如線程執(zhí)行 sleep 方法,讓出CPU。 使用硬件中斷的方式引起上下文切換

3
?線程安全性
在 Java 中,要實現(xiàn)線程安全性,必須要正確的使用線程和鎖,但是這些只是滿足線程安全的一種方式,要編寫正確無誤的線程安全的代碼,其核心就是對狀態(tài)訪問操作進行管理。最重要的就是最 共享(Shared)的 和 可變(Mutable)的狀態(tài)。只有共享和可變的變量才會出現(xiàn)問題,私有變量不會出現(xiàn)問題,參考程序計數(shù)器。
對象的狀態(tài)可以理解為存儲在實例變量或者靜態(tài)變量中的數(shù)據(jù),共享意味著某個變量可以被多個線程同時訪問、可變意味著變量在生命周期內(nèi)會發(fā)生變化。一個變量是否是線程安全的,取決于它是否被多個線程訪問。要使變量能夠被安全訪問,必須通過同步機制來對變量進行修飾。
如果不采用同步機制的話,那么就要避免多線程對共享變量的訪問,主要有下面兩種方式
不要在多線程之間共享變量 將共享變量置為不可變的
我們說了這么多次線程安全性,那么什么是線程安全性呢?
什么是線程安全性
多個線程可以同時安全調(diào)用的代碼稱為線程安全的,如果一段代碼是安全的,那么這段代碼就不存在 競態(tài)條件。僅僅當多個線程共享資源時,才會出現(xiàn)競態(tài)條件。
根據(jù)上面的探討,我們可以得出一個簡單的結(jié)論:「當多個線程訪問某個類時,這個類始終都能表現(xiàn)出正確的行為,那么就稱這個類是線程安全的」。
單線程就是一個線程數(shù)量為 1 的多線程,單線程一定是線程安全的。讀取某個變量的值不會產(chǎn)生安全性問題,因為不管讀取多少次,這個變量的值都不會被修改。
原子性
我們上面提到了原子性的概念,你可以把原子性操作想象成為一個不可分割 的整體,它的結(jié)果只有兩種,要么全部執(zhí)行,要么全部回滾。你可以把原子性認為是 婚姻關(guān)系 的一種,男人和女人只會產(chǎn)生兩種結(jié)果,好好的 和 說散就散,一般男人的一生都可以把他看成是原子性的一種,當然我們不排除時間管理(線程切換)的個例,我們知道線程切換必然會伴隨著安全性問題,男人要出去浪也會造成兩種結(jié)果,這兩種結(jié)果分別對應(yīng)安全性的兩個結(jié)果:線程安全(好好的)和線程不安全(說散就散)。
競態(tài)條件
有了上面的線程切換的功底,那么競態(tài)條件也就好定義了,它指的就是「兩個或多個線程同時對一共享數(shù)據(jù)進行修改,從而影響程序運行的正確性時,這種就被稱為競態(tài)條件(race condition)」 ,線程切換是導致競態(tài)條件出現(xiàn)的誘導因素,我們通過一個示例來說明,來看一段代碼
public?class?RaceCondition?{
??
??private?Signleton?single?=?null;
??public?Signleton?newSingleton(){
????if(single?==?null){
??????single?=?new?Signleton();
????}
????return?single;
??}
??
}
在上面的代碼中,涉及到一個競態(tài)條件,那就是判斷 single 的時候,如果 single 判斷為空,此時發(fā)生了線程切換,另外一個線程執(zhí)行,判斷 single 的時候,也是空,執(zhí)行 new 操作,然后線程切換回之前的線程,再執(zhí)行 new 操作,那么內(nèi)存中就會有兩個 Singleton 對象。
加鎖機制
在 Java 中,有很多種方式來對共享和可變的資源進行加鎖和保護。Java 提供一種內(nèi)置的機制對資源進行保護:synchronized 關(guān)鍵字,它有三種保護機制
對方法進行加鎖,確保多個線程中只有一個線程執(zhí)行方法; 對某個對象實例(在我們上面的探討中,變量可以使用對象來替換)進行加鎖,確保多個線程中只有一個線程對對象實例進行訪問; 對類對象進行加鎖,確保多個線程只有一個線程能夠訪問類中的資源。
synchronized 關(guān)鍵字對資源進行保護的代碼塊俗稱 同步代碼塊(Synchronized Block),例如
synchronized(lock){
??//?線程安全的代碼
}
每個 Java 對象都可以用做一個實現(xiàn)同步的鎖,這些鎖被稱為 內(nèi)置鎖(Instrinsic Lock)或者 監(jiān)視器鎖(Monitor Lock)。線程在進入同步代碼之前會自動獲得鎖,并且在退出同步代碼時自動釋放鎖,而無論是通過正常執(zhí)行路徑退出還是通過異常路徑退出,獲得內(nèi)置鎖的唯一途徑就是進入這個由鎖保護的同步代碼塊或方法。
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() 方法鎖的線程可以執(zhí)行 doSomethingElse() 方法,執(zhí)行完畢后可以重新執(zhí)行 doSomething() 方法中的內(nèi)容。鎖重入也支持子類和父類之間的重入,具體的我們后面會進行介紹。
volatile 是一種輕量級的 synchronized,也就是一種輕量級的加鎖方式,volatile 通過保證共享變量的可見性來從側(cè)面對對象進行加鎖。可見性的意思就是當一個線程修改一個共享變量時,另外一個線程能夠 看見 這個修改的值。volatile 的執(zhí)行成本要比 synchronized 低很多,因為 volatile 不會引起線程的上下文切換。

我們還可以使用原子類 來保證線程安全,原子類其實就是 rt.jar 下面以 atomic 開頭的類

除此之外,我們還可以使用 java.util.concurrent 工具包下的線程安全的集合類來確保線程安全,具體的實現(xiàn)類和其原理我們后面會說。
可以使用不同的并發(fā)模型來實現(xiàn)并發(fā)系統(tǒng),并發(fā)模型說的是系統(tǒng)中的線程如何協(xié)作完成并發(fā)任務(wù)。不同的并發(fā)模型以不同的方式拆分任務(wù),線程可以以不同的方式進行通信和協(xié)作。

4
?競態(tài)條件和關(guān)鍵區(qū)域?
競態(tài)條件是在關(guān)鍵代碼區(qū)域發(fā)生的一種特殊條件。關(guān)鍵區(qū)域是由多個線程同時執(zhí)行的代碼部分,關(guān)鍵區(qū)域中的代碼執(zhí)行順序會對造成不一樣的結(jié)果。如果多個線程執(zhí)行一段關(guān)鍵代碼,而這段關(guān)鍵代碼會因為執(zhí)行順序不同而造成不同的結(jié)果時,那么這段代碼就會包含競爭條件。

5
?并發(fā)模型和分布式系統(tǒng)很相似
并發(fā)模型其實和分布式系統(tǒng)模型非常相似,在并發(fā)模型中是線程彼此進行通信,而在分布式系統(tǒng)模型中是 進程 彼此進行通信。然而本質(zhì)上,進程和線程也非常相似。這也就是為什么并發(fā)模型和分布式模型非常相似的原因。
分布式系統(tǒng)通常要比并發(fā)系統(tǒng)面臨更多的挑戰(zhàn)和問題比如進程通信、網(wǎng)絡(luò)可能出現(xiàn)異常,或者遠程機器掛掉等等。但是一個并發(fā)模型同樣面臨著比如 CPU 故障、網(wǎng)卡出現(xiàn)問題、硬盤出現(xiàn)問題等。
因為并發(fā)模型和分布式模型很相似,因此他們可以相互借鑒,例如用于線程分配的模型就類似于分布式系統(tǒng)環(huán)境中的負載均衡模型。
其實說白了,分布式模型的思想就是借鑒并發(fā)模型的基礎(chǔ)上推演發(fā)展來的。

6
?認識兩個狀態(tài)?
并發(fā)模型的一個重要的方面是,線程是否應(yīng)該共享狀態(tài),是具有共享狀態(tài)還是獨立狀態(tài)。共享狀態(tài)也就意味著在不同線程之間共享某些狀態(tài)
狀態(tài)其實就是數(shù)據(jù),比如一個或者多個對象。當線程要共享數(shù)據(jù)時,就會造成 競態(tài)條件 或者 死鎖 等問題。當然,這些問題只是可能會出現(xiàn),具體實現(xiàn)方式取決于你是否安全的使用和訪問共享對象。

獨立的狀態(tài)表明狀態(tài)不會在多個線程之間共享,如果線程之間需要通信的話,他們可以訪問不可變的對象來實現(xiàn),這是最有效的避免并發(fā)問題的一種方式,如下圖所示

使用獨立狀態(tài)讓我們的設(shè)計更加簡單,因為只有一個線程能夠訪問對象,即使交換對象,也是不可變的對象。

7
?并發(fā)模型?
并行 Worker
第一個并發(fā)模型是并行 worker 模型,客戶端會把任務(wù)交給 代理人(Delegator),然后由代理人把工作分配給不同的 工人(worker)。如下圖所示

并行 worker 的核心思想是,它主要有兩個進程即代理人和工人,Delegator 負責接收來自客戶端的任務(wù)并把任務(wù)下發(fā),交給具體的 Worker 進行處理,Worker 處理完成后把結(jié)果返回給 Delegator,在 Delegator 接收到 Worker 處理的結(jié)果后對其進行匯總,然后交給客戶端。
并行 Worker 模型是 Java 并發(fā)模型中非常常見的一種模型。許多 java.util.concurrent 包下的并發(fā)工具都使用了這種模型。
并行 Worker 的優(yōu)點
并行 Worker 模型的一個非常明顯的特點就是很容易理解,為了提高系統(tǒng)的并行度你可以增加多個 Worker 完成任務(wù)。
并行 Worker 模型的另外一個好處就是,它會將一個任務(wù)拆分成多個小任務(wù),并發(fā)執(zhí)行,Delegator 在接受到 Worker 的處理結(jié)果后就會返回給 Client,整個 Worker -> Delegator -> Client 的過程是異步的。
并行 Worker 的缺點
同樣的,并行 Worker 模式同樣會有一些隱藏的缺點
「共享狀態(tài)會變得很復雜」
實際的并行 Worker 要比我們圖中畫出的更復雜,主要是并行 Worker 通常會訪問內(nèi)存或共享數(shù)據(jù)庫中的某些共享數(shù)據(jù)。

這些共享狀態(tài)可能會使用一些工作隊列來保存業(yè)務(wù)數(shù)據(jù)、數(shù)據(jù)緩存、數(shù)據(jù)庫的連接池等。在線程通信中,線程需要確保共享狀態(tài)是否能夠讓其他線程共享,而不是僅僅停留在 CPU 緩存中讓自己可用,當然這些都是程序員在設(shè)計時就需要考慮的問題。線程需要避免 競態(tài)條件,死鎖 和許多其他共享狀態(tài)造成的并發(fā)問題。
多線程在訪問共享數(shù)據(jù)時,會丟失并發(fā)性,因為操作系統(tǒng)要保證只有一個線程能夠訪問數(shù)據(jù),這會導致共享數(shù)據(jù)的爭用和搶占。未搶占到資源的線程會 阻塞。
現(xiàn)代的非阻塞并發(fā)算法可以減少爭用提高性能,但是非阻塞算法比較難以實現(xiàn)。
可持久化的數(shù)據(jù)結(jié)構(gòu)(Persistent data structures) 是另外一個選擇。可持久化的數(shù)據(jù)結(jié)構(gòu)在修改后始終會保留先前版本。因此,如果多個線程同時修改一個可持久化的數(shù)據(jù)結(jié)構(gòu),并且一個線程對其進行了修改,則修改的線程會獲得對新數(shù)據(jù)結(jié)構(gòu)的引用。
雖然可持久化的數(shù)據(jù)結(jié)構(gòu)是一個新的解決方法,但是這種方法實行起來卻有一些問題,比如,一個持久列表會將新元素添加到列表的開頭,并返回所添加的新元素的引用,但是其他線程仍然只持有列表中先前的第一個元素的引用,他們看不到新添加的元素。
持久化的數(shù)據(jù)結(jié)構(gòu)比如 鏈表(LinkedList) 在硬件性能上表現(xiàn)不佳。列表中的每個元素都是一個對象,這些對象散布在計算機內(nèi)存中。現(xiàn)代 CPU 的順序訪問往往要快的多,因此使用數(shù)組等順序訪問的數(shù)據(jù)結(jié)構(gòu)則能夠獲得更高的性能。CPU 高速緩存可以將一個大的矩陣塊加載到高速緩存中,并讓 CPU 在加載后直接訪問 CPU 高速緩存中的數(shù)據(jù)。對于鏈表,將元素分散在整個 RAM 上,這實際上是不可能的。
「無狀態(tài)的 worker」
共享狀態(tài)可以由其他線程所修改,因此,worker 必須在每次操作共享狀態(tài)時重新讀取,以確保在副本上能夠正確工作。不在線程內(nèi)部保持狀態(tài)的 worker 成為無狀態(tài)的 worker。
「作業(yè)順序是不確定的」
并行工作模型的另一個缺點是作業(yè)的順序不確定,無法保證首先執(zhí)行或最后執(zhí)行哪些作業(yè)。任務(wù) A 在任務(wù) B 之前分配給 worker,但是任務(wù) B 可能在任務(wù) A 之前執(zhí)行。
流水線
第二種并發(fā)模型就是我們經(jīng)常在生產(chǎn)車間遇到的 流水線并發(fā)模型,下面是流水線設(shè)計模型的流程圖

這種組織架構(gòu)就像是工廠中裝配線中的 worker,每個 worker 只完成全部工作的一部分,完成一部分后,worker 會將工作轉(zhuǎn)發(fā)給下一個 worker。
每道程序都在自己的線程中運行,彼此之間不會共享狀態(tài),這種模型也被稱為無共享并發(fā)模型。
使用流水線并發(fā)模型通常被設(shè)計為非阻塞I/O,也就是說,當沒有給 worker 分配任務(wù)時,worker 會做其他工作。非阻塞I/O 意味著當 worker 開始 I/O 操作,例如從網(wǎng)絡(luò)中讀取文件,worker 不會等待 I/O 調(diào)用完成。因為 I/O 操作很慢,所以等待 I/O 非常耗費時間。在等待 I/O 的同時,CPU 可以做其他事情,I/O 操作完成后的結(jié)果將傳遞給下一個 worker。下面是非阻塞 I/O 的流程圖

在實際情況中,任務(wù)通常不會按著一條裝配線流動,由于大多數(shù)程序需要做很多事情,因此需要根據(jù)完成的不同工作在不同的 worker 之間流動,如下圖所示

任務(wù)還可能需要多個 worker 共同參與完成

響應(yīng)式 - 事件驅(qū)動系統(tǒng)
使用流水線模型的系統(tǒng)有時也被稱為 響應(yīng)式 或者 事件驅(qū)動系統(tǒng),這種模型會根據(jù)外部的事件作出響應(yīng),事件可能是某個 HTTP 請求或者某個文件完成加載到內(nèi)存中。
Actor 模型
在 Actor 模型中,每一個 Actor 其實就是一個 Worker, 每一個 Actor 都能夠處理任務(wù)。
簡單來說,Actor 模型是一個并發(fā)模型,它定義了一系列系統(tǒng)組件應(yīng)該如何動作和交互的通用規(guī)則,最著名的使用這套規(guī)則的編程語言是 Erlang。一個參與者Actor對接收到的消息做出響應(yīng),然后可以創(chuàng)建出更多的 Actor 或發(fā)送更多的消息,同時準備接收下一條消息。

Channels 模型
在 Channel 模型中,worker 通常不會直接通信,與此相對的,他們通常將事件發(fā)送到不同的 通道(Channel)上,然后其他 worker 可以在這些通道上獲取消息,下面是 Channel 的模型圖

有的時候 worker 不需要明確知道接下來的 worker 是誰,他們只需要將作者寫入通道中,監(jiān)聽 Channel 的 worker 可以訂閱或者取消訂閱,這種方式降低了 worker 和 worker 之間的耦合性。
流水線設(shè)計的優(yōu)點
與并行設(shè)計模型相比,流水線模型具有一些優(yōu)勢,具體優(yōu)勢如下
「不會存在共享狀態(tài)」
因為流水線設(shè)計能夠保證 worker 在處理完成后再傳遞給下一個 worker,所以 worker 與 worker 之間不需要共享任何狀態(tài),也就無需考慮并發(fā)問題。你甚至可以在實現(xiàn)上把每個 worker 看成是單線程的一種。
「有狀態(tài) worker」
因為 worker 知道沒有其他線程修改自身的數(shù)據(jù),所以流水線設(shè)計中的 worker 是有狀態(tài)的,有狀態(tài)的意思是他們可以將需要操作的數(shù)據(jù)保留在內(nèi)存中,有狀態(tài)通常比無狀態(tài)更快。
「更好的硬件整合」
因為你可以把流水線看成是單線程的,而單線程的工作優(yōu)勢在于它能夠和硬件的工作方式相同。因為有狀態(tài)的 worker 通常在 CPU 中緩存數(shù)據(jù),這樣可以更快地訪問緩存的數(shù)據(jù)。
「使任務(wù)更加有效的進行」
可以對流水線并發(fā)模型中的任務(wù)進行排序,一般用來日志的寫入和恢復。
流水線設(shè)計的缺點
流水線并發(fā)模型的缺點是任務(wù)會涉及多個 worker,因此可能會分散在項目代碼的多個類中。因此很難確定每個 worker 都在執(zhí)行哪個任務(wù)。流水線的代碼編寫也比較困難,設(shè)計許多嵌套回調(diào)處理程序的代碼通常被稱為 回調(diào)地獄。回調(diào)地獄很難追蹤 debug。

8
?函數(shù)性并行
函數(shù)性并行模型是最近才提出的一種并發(fā)模型,它的基本思路是使用函數(shù)調(diào)用來實現(xiàn)。消息的傳遞就相當于是函數(shù)的調(diào)用。傳遞給函數(shù)的參數(shù)都會被拷貝,因此在函數(shù)之外的任何實體都無法操縱函數(shù)內(nèi)的數(shù)據(jù)。這使得函數(shù)執(zhí)行類似于原子操作。每個函數(shù)調(diào)用都可以獨立于任何其他函數(shù)調(diào)用執(zhí)行。
當每個函數(shù)調(diào)用獨立執(zhí)行時,每個函數(shù)都可以在單獨的 CPU 上執(zhí)行。這也就是說,函數(shù)式并行并行相當于是各個 CPU 單獨執(zhí)行各自的任務(wù)。
JDK 1.7 中的 ForkAndJoinPool 類就實現(xiàn)了函數(shù)性并行的功能。Java 8 提出了 stream 的概念,使用并行流也能夠?qū)崿F(xiàn)大量集合的迭代。
函數(shù)性并行的難點是要知道函數(shù)的調(diào)用流程以及哪些 CPU 執(zhí)行了哪些函數(shù),跨 CPU 函數(shù)調(diào)用會帶來額外的開銷。
我們之前說過,線程就是進程中的一條順序流,在 Java 中,每一條 Java 線程就像是 JVM 的一條順序流,就像是虛擬 CPU 一樣來執(zhí)行代碼。Java 中的 main() 方法是一條特殊的線程,JVM 創(chuàng)建的 main 線程是一條主執(zhí)行線程,在 Java 中,方法都是由 main 方法發(fā)起的。在 main 方法中,你照樣可以創(chuàng)建其他的線程(執(zhí)行順序流),這些線程可以和 main 方法共同執(zhí)行應(yīng)用代碼。
Java 線程也是一種對象,它和其他對象一樣。Java 中的 Thread 表示線程,Thread 是 java.lang.Thread 類或其子類的實例。那么下面我們就來一起探討一下在 Java 中如何創(chuàng)建和啟動線程。

9
?創(chuàng)建并啟動線程?
在 Java 中,創(chuàng)建線程的方式主要有三種
通過繼承 Thread類來創(chuàng)建線程通過實現(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);
????}
}
線程的主要創(chuàng)建步驟如下
定義一個線程類使其繼承 Thread 類,并重寫其中的 run 方法,run 方法內(nèi)部就是線程要完成的任務(wù),因此 run 方法也被稱為 執(zhí)行體創(chuàng)建了 Thread 的子類,上面代碼中的子類是 TJavaThread啟動方法需要注意,并不是直接調(diào)用 run方法來啟動線程,而是使用start方法來啟動線程。當然 run 方法可以調(diào)用,這樣的話就會變成普通方法調(diào)用,而不是新創(chuàng)建一個線程來調(diào)用了。
public?static?void?main(String[]?args)?throws?InterruptedException?{
??TJavaThread?tJavaThread?=?new?TJavaThread();
??tJavaThread.run();
??System.out.println("count?=?"?+?count);
}
這樣的話,整個 main 方法只有一條執(zhí)行線程也就是 main 線程,由兩條執(zhí)行線程變?yōu)橐粭l執(zhí)行線程

Thread 構(gòu)造器只需要一個 Runnable 對象,調(diào)用 Thread 對象的 start() 方法為該線程執(zhí)行必須的初始化操作,然后調(diào)用 Runnable 的 run 方法,以便在這個線程中啟動任務(wù)。我們上面使用了線程的 join 方法,它用來等待線程的執(zhí)行結(jié)束,如果我們不加 join 方法,它就不會等待 tJavaThread 的執(zhí)行完畢,輸出的結(jié)果可能就不是 10000

可以看到,在 run ?方法還沒有結(jié)束前,run 就被返回了。也就是說,程序不會等到 run 方法執(zhí)行完畢就會執(zhí)行下面的指令。
使用繼承方式創(chuàng)建線程的優(yōu)勢:編寫比較簡單;可以使用 this 關(guān)鍵字直接指向當前線程,而無需使用 Thread.currentThread() 來獲取當前線程。
使用繼承方式創(chuàng)建線程的劣勢:在 Java 中,只允許單繼承(拒絕肛精說使用內(nèi)部類可以實現(xiàn)多繼承)的原則,所以使用繼承的方式,子類就不能再繼承其他類。
使用 Runnable 接口來創(chuàng)建線程
相對的,還可以使用 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);
????}
}
線程的主要創(chuàng)建步驟如下
首先定義 Runnable 接口,并重寫 Runnable 接口的 run 方法,run 方法的方法體同樣是該線程的線程執(zhí)行體。 創(chuàng)建線程實例,可以使用上面代碼這種簡單的方式創(chuàng)建,也可以通過 new 出線程的實例來創(chuàng)建,如下所示
TJavaThreadUseImplements?tJavaThreadUseImplements?=?new?TJavaThreadUseImplements();
new?Thread(tJavaThreadUseImplements).start();
再調(diào)用線程對象的 start 方法來啟動該線程。
線程在使用實現(xiàn) Runnable 的同時也能實現(xiàn)其他接口,非常適合多個相同線程來處理同一份資源的情況,體現(xiàn)了面向?qū)ο蟮乃枷搿?/p>
使用 Runnable 實現(xiàn)的劣勢是編程稍微繁瑣,如果要訪問當前線程,則必須使用 Thread.currentThread() 方法。
使用 Callable 接口來創(chuàng)建線程
Runnable 接口執(zhí)行的是獨立的任務(wù),Runnable 接口不會產(chǎn)生任何返回值,如果你希望在任務(wù)完成后能夠返回一個值的話,那么你可以實現(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?task?=?new?FutureTask((Callable)?()?->?{
????????????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)多個接口,也能夠得到執(zhí)行結(jié)果的返回值。Callable 和 Runnable 接口還是有一些區(qū)別的,主要區(qū)別如下
Callable 執(zhí)行的任務(wù)有返回值,而 Runnable 執(zhí)行的任務(wù)沒有返回值 Callable(重寫)的方法是 call 方法,而 Runnable(重寫)的方法是 run 方法。 call 方法可以拋出異常,而 Runnable 方法不能拋出異常
使用線程池來創(chuàng)建線程
首先先來認識一下頂級接口 Executor,Executor 雖然不是傳統(tǒng)線程創(chuàng)建的方式之一,但是它卻成為了創(chuàng)建線程的替代者,使用線程池的好處如下
利用線程池能夠復用線程、控制最大并發(fā)數(shù)。 實現(xiàn)任務(wù)線程隊列 緩存策略和拒絕機制。實現(xiàn)某些與時間相關(guān)的功能,如定時執(zhí)行、周期執(zhí)行等。 隔離線程環(huán)境。比如,交易服務(wù)和搜索服務(wù)在同一臺服務(wù)器上,分別開啟兩個線程池,交易線程的資源消耗明顯要大;因此,通過配置獨立的線程池,將較慢的交易服務(wù)與搜索服務(wù)隔開,避免服務(wù)線程互相影響。
你可以使用如下操作來替換線程創(chuàng)建
new?Thread(new(RunnableTask())).start()
//?替換為
??
Executor?executor?=?new?ExecutorSubClass()?//?線程池實現(xiàn)類;
executor.execute(new?RunnableTask1());
executor.execute(new?RunnableTask2());
ExecutorService 是 Executor 的默認實現(xiàn),也是 Executor 的擴展接口,ThreadPoolExecutor 類提供了線程池的擴展實現(xiàn)。Executors 類為這些 Executor 提供了方便的工廠方法。下面是使用 ExecutorService 創(chuàng)建線程的幾種方式
CachedThreadPool
從而簡化了并發(fā)編程。Executor 在客戶端和任務(wù)之間提供了一個間接層;與客戶端直接執(zhí)行任務(wù)不同,這個中介對象將執(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 會為每個任務(wù)都創(chuàng)建一個線程。
?注意:ExecutorService 對象是使用靜態(tài)的
?Executors創(chuàng)建的,這個方法可以確定 Executor 類型。對shutDown的調(diào)用可以防止新任務(wù)提交給 ExecutorService ,這個線程在 Executor 中所有任務(wù)完成后退出。
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 使你可以一次性的預先執(zhí)行高昂的線程分配,因此也就可以限制線程的數(shù)量。這可以節(jié)省時間,因為你不必為每個任務(wù)都固定的付出創(chuàng)建線程的開銷。
SingleThreadExecutor
SingleThreadExecutor 就是線程數(shù)量為 1的 FixedThreadPool,如果向 SingleThreadPool 一次性提交了多個任務(wù),那么這些任務(wù)將會排隊,每個任務(wù)都會在下一個任務(wù)開始前結(jié)束,所有的任務(wù)都將使用相同的線程。SingleThreadPool 會序列化所有提交給他的任務(wù),并會維護它自己(隱藏)的懸掛隊列。
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ù)分配了五個線程,但是這五個線程不像是我們之前看到的有換進換出的效果,它每次都會先執(zhí)行完自己的那個線程,然后余下的線程繼續(xù)走完這條線程的執(zhí)行路徑。你可以用 SingleThreadExecutor 來確保任意時刻都只有唯一一個任務(wù)在運行。
休眠
影響任務(wù)行為的一種簡單方式就是使線程 休眠,選定給定的休眠時間,調(diào)用它的 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();
????}
}
?關(guān)于 TimeUnit 中的 sleep() 方法和 Thread.sleep() 方法的比較,請參考下面這篇博客
(https://www.cnblogs.com/xiadongqing/p/9925567.html)
?
優(yōu)先級
上面提到線程調(diào)度器對每個線程的執(zhí)行都是不可預知的,隨機執(zhí)行的,那么有沒有辦法告訴線程調(diào)度器哪個任務(wù)想要優(yōu)先被執(zhí)行呢?你可以通過設(shè)置線程的優(yōu)先級狀態(tài),告訴線程調(diào)度器哪個線程的執(zhí)行優(yōu)先級比較高,「請給這個騎手馬上派單」,線程調(diào)度器傾向于讓優(yōu)先級較高的線程優(yōu)先執(zhí)行,然而,這并不意味著優(yōu)先級低的線程得不到執(zhí)行,也就是說,優(yōu)先級不會導致死鎖的問題。優(yōu)先級較低的線程只是執(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() 方法來打印線程的名稱。你可以改寫線程的默認輸出,這里采用了 「Thread[pool-1-thread-1,10,main]」 這種形式的輸出。
通過輸出,你可以看到,最后一個線程的優(yōu)先級最低,其余的線程優(yōu)先級最高。注意,優(yōu)先級是在 run 開頭設(shè)置的,在構(gòu)造器中設(shè)置它們不會有任何好處,因為這個時候線程還沒有執(zhí)行任務(wù)。
盡管 JDK 有 10 個優(yōu)先級,但是一般只有「MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY」 三種級別。
作出讓步
我們上面提過,如果知道一個線程已經(jīng)在 run() 方法中運行的差不多了,那么它就可以給線程調(diào)度器一個提示:我已經(jīng)完成了任務(wù)中最重要的部分,可以讓給別的線程使用 CPU 了。這個暗示將通過 yield() 方法作出。
?有一個很重要的點就是,Thread.yield() 是建議執(zhí)行切換CPU,而不是強制執(zhí)行CPU切換。
?
對于任何重要的控制或者在調(diào)用應(yīng)用時,都不能依賴于 yield()方法,實際上, yield() 方法經(jīng)常被濫用。
后臺線程
后臺(daemon)線程,是指運行時在后臺提供的一種服務(wù)線程,這種線程不是屬于必須的。當所有非后臺線程結(jié)束時,程序也就停止了,**同時會終止所有的后臺線程。**反過來說,只要有任何非后臺線程還在運行,程序就不會終止。
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)中會創(chuàng)建 10 個線程,并把每個線程設(shè)置為后臺線程,然后開始運行,for 循環(huán)會進行十次,然后輸出信息,隨后主線程睡眠一段時間后停止運行。在每次 run 循環(huán)中,都會打印當前線程的信息,主線程運行完畢,程序就執(zhí)行完畢了。因為 daemon 是后臺線程,無法影響主線程的執(zhí)行。
但是當你把 daemon.setDaemon(true) 去掉時,while(true) 會進行無限循環(huán),那么主線程一直在執(zhí)行最重要的任務(wù),所以會一直循環(huán)下去無法停止。
ThreadFactory
按需要創(chuàng)建線程的對象。使用線程工廠替換了 Thread 或者 Runnable 接口的硬連接,使程序能夠使用特殊的線程子類,優(yōu)先級等。一般的創(chuàng)建方式為
class?SimpleThreadFactory?implements?ThreadFactory?{
??public?Thread?newThread(Runnable?r)?{
????return?new?Thread(r);
??}
}
?Executors.defaultThreadFactory 方法提供了一個更有用的簡單實現(xiàn),它在返回之前將創(chuàng)建的線程上下文設(shè)置為已知值
?
ThreadFactory是一個接口,它只有一個方法就是創(chuàng)建線程的方法
public?interface?ThreadFactory?{
????//?構(gòu)建一個新的線程。實現(xiàn)類可能初始化優(yōu)先級,名稱,后臺線程狀態(tài)和?線程組等
????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 可以接受一個線程池對象,創(chuàng)建一個根據(jù)需要創(chuàng)建新線程的線程池,但會在它們可用時重用先前構(gòu)造的線程,并在需要時使用提供的 ThreadFactory 創(chuàng)建新線程。
public?static?ExecutorService?newCachedThreadPool(ThreadFactory?threadFactory)?{
??return?new?ThreadPoolExecutor(0,?Integer.MAX_VALUE,
????????????????????????????????60L,?TimeUnit.SECONDS,
????????????????????????????????new?SynchronousQueue(),
????????????????????????????????threadFactory);
}
加入一個線程
一個線程可以在其他線程上調(diào)用 join() 方法,其效果是等待一段時間直到第二個線程結(jié)束才正常執(zhí)行。如果某個線程在另一個線程 t 上調(diào)用 t.join() 方法,此線程將被掛起,直到目標線程 t 結(jié)束才回復(可以用 t.isAlive() 返回為真假判斷)。
也可以在調(diào)用 join 時帶上一個超時參數(shù),來設(shè)置到期時間,時間到期,join方法自動返回。
對 join 的調(diào)用也可以被中斷,做法是在線程上調(diào)用 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() 方法等待線程死亡。換句話說,它會導致當前運行的線程停止執(zhí)行,直到它加入的線程完成其任務(wù)。
線程異常捕獲
由于線程的本質(zhì),使你不能捕獲從線程中逃逸的異常,一旦異常逃出任務(wù)的 run 方法,它就會向外傳播到控制臺,除非你采取特殊的步驟捕獲這種錯誤的異常,在 Java5 之前,你可以通過線程組來捕獲,但是在 Java 5 之后,就需要用 Executor 來解決問題,因為線程組不是一次好的嘗試。
下面的任務(wù)會在 run 方法的執(zhí)行期間拋出一個異常,并且這個異常會拋到 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 產(chǎn)生線程的方式,Java5 提供了一個新的接口 Thread.UncaughtExceptionHandler ,它允許你在每個 Thread 上都附著一個異常處理器。Thread.UncaughtExceptionHandler.uncaughtException() 會在線程因未捕獲臨近死亡時被調(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());
??????
???????//?手動拋出異常
????????throw?new?RuntimeException();
????}
}
//?實現(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());
????}
}
在程序中添加了額外的追蹤機制,用來驗證工廠創(chuàng)建的線程會傳遞給UncaughtExceptionHandler,你可以看到,未捕獲的異常是通過 uncaughtException 來捕獲的。
往期精選

