我為了扒了并發(fā),寫了 2 萬字 40 張圖

1
?并發(fā)歷史?
在計(jì)算機(jī)最早期的時(shí)候,沒有操作系統(tǒng),執(zhí)行程序只需要一種方式,那就是從頭到尾依次執(zhí)行。任何資源都會為這個(gè)程序服務(wù),在計(jì)算機(jī)使用某些資源時(shí),其他資源就會空閑,就會存在 浪費(fèi)資源 的情況。
?這里說的浪費(fèi)資源指的是資源空閑,沒有充分使用的情況。
?
操作系統(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)和用戶程序被啟動交替、穿插地執(zhí)行。操作系統(tǒng)在協(xié)調(diào)和分配進(jìn)程的同時(shí),操作系統(tǒng)也會為不同進(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)限賦予完畢后才能接受外部輸入??偟膩碇v,就是在等待程序時(shí)無法執(zhí)行其他工作。如果在等待程序時(shí)可以運(yùn)行另一個(gè)程序,那么將會大大提高資源的利用率。(資源并不會覺得累)因?yàn)樗粫澦?/section>公平性,不同的用戶和程序都能夠使用計(jì)算機(jī)上的資源。一種高效的運(yùn)行方式是為不同的程序劃分時(shí)間片來使用資源,但是有一點(diǎn)需要注意,操作系統(tǒng)可以決定不同進(jìn)程的優(yōu)先級。雖然每個(gè)進(jìn)程都有能夠公平享有資源的權(quán)利,但是當(dāng)有一個(gè)進(jìn)程釋放資源后的同時(shí)有一個(gè)優(yōu)先級更高的進(jìn)程搶奪資源,就會造成優(yōu)先級低的進(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)勢在于其「直觀性和簡單性」,客觀來講,串行編程更適合我們?nèi)四X的思考方式,但是我們并不會滿足于順序編程,「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)程中會有多個(gè)線程來完成一些任務(wù),這些任務(wù)有可能相同有可能不同。每個(gè)線程都有自己的執(zhí)行順序。

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

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

2
?線程?
什么是多線程
多線程意味著你能夠在同一個(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)用程序會執(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ù),合理編寫一道準(zhǔn)確無誤的多線程程序更是一門藝術(shù),如果線程使用得當(dāng),能夠有效的降低程序的開發(fā)和維護(hù)成本。
Java 很好的在用戶空間實(shí)現(xiàn)了開發(fā)工具包,并在內(nèi)核空間提供系統(tǒng)調(diào)用來支持多線程編程,Java 支持了豐富的類庫 java.util.concurrent 和跨平臺的內(nèi)存模型,同時(shí)也提高了開發(fā)人員的門檻,并發(fā)一直以來是一個(gè)高階的主題,但是現(xiàn)在,并發(fā)也成為了主流開發(fā)人員的必備素質(zhì)。
雖然線程帶來的好處很多,但是編寫正確的多線程(并發(fā))程序是一件極困難的事情,并發(fā)程序的 Bug 往往會詭異地出現(xiàn)又詭異的消失,在當(dāng)你認(rèn)為沒有問題的時(shí)候它就出現(xiàn)了,難以定位 是并發(fā)程序的一個(gè)特征,所以在此基礎(chǔ)上你需要有扎實(shí)的并發(fā)基本功。那么,并發(fā)為什么會出現(xiàn)呢?
并發(fā)為什么會出現(xiàn)
計(jì)算機(jī)世界的快速發(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ù)漏桶理論來說,程序整體的性能取決于最慢的操作也就是磁盤訪問速度。
因?yàn)?CPU 速度太快了,所以為了發(fā)揮 CPU 的速度優(yōu)勢,平衡這三者的速度差異,計(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í)行順序,讓緩存能夠合理的使用
我們在享受這些便利的同時(shí),多線程也為我們帶來了挑戰(zhàn),下面我們就來探討一下并發(fā)問題為什么會出現(xiàn)以及多線程的源頭是什么
線程帶來的安全性問題
線程安全性是非常復(fù)雜的,在沒有采用同步機(jī)制的情況下,多個(gè)線程中的執(zhí)行操作往往是不可預(yù)測的,這也是多線程帶來的挑戰(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 的值每次都不一樣,這不符合我們的預(yù)測,那么為什么會出現(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)存之間
如果用圖來表示的話我想會是下面這樣

在多核時(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è)線程會并發(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」,這怎么就會產(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è)線程都會把 i 的值讀入到 CPU 緩存中,然后執(zhí)行 + 1 操作,再把 + 1 之后的值寫入內(nèi)存。因?yàn)榫€程間都有各自的虛擬機(jī)棧和程序計(jì)數(shù)器,他們彼此之間沒有數(shù)據(jù)交換,所以當(dāng) aThread 執(zhí)行 + 1 操作后,會把數(shù)據(jù)寫入到內(nèi)存,同時(shí) bThread 執(zhí)行 + 1 操作后,也會把數(shù)據(jù)寫入到內(nèi)存,因?yàn)?CPU 時(shí)間片的執(zhí)行周期是不確定的,所以會出現(xiàn)當(dāng) aThread 還沒有把數(shù)據(jù)寫入內(nèi)存時(shí),bThread 就會讀取內(nèi)存中的數(shù)據(jù),然后執(zhí)行 + 1操作,再寫回內(nèi)存,從而覆蓋 i 的值,導(dǎo)致 aThread 所做的努力白費(fèi)。

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

可以看到,當(dāng) aThread 在執(zhí)行完整個(gè) i++ 的操作后,操作系統(tǒng)對線程進(jìn)行切換,由 aThread -> bThread,這是最理想的操作,一旦操作系統(tǒng)在任意 讀取/增加/寫入 階段產(chǎn)生線程切換,都會產(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ī)硬件,編譯器和庫也提供原子性操作。
在加載和存儲中,計(jì)算機(jī)硬件對存儲器字進(jìn)行讀取和寫入。為了對值進(jìn)行匹配、增加或者減小操作,一般通過原子操作進(jìn)行。在原子操作期間,處理器可以在同一數(shù)據(jù)傳輸期間完成讀取和寫入。這樣,其他輸入/輸出機(jī)制或處理器無法執(zhí)行存儲器讀取或?qū)懭肴蝿?wù),直到原子操作完成為止。
?
簡單來講,就是「原子操作要么全部執(zhí)行,要么全部不執(zhí)行」。數(shù)據(jù)庫事務(wù)的原子性也是基于這個(gè)概念演進(jìn)的。
有序性問題
在并發(fā)編程中還有帶來讓人非常頭疼的 有序性 問題,有序性顧名思義就是順序性,在計(jì)算機(jī)中指的就是指令的先后執(zhí)行順序。一個(gè)非常顯而易見的例子就是 JVM 中的類加載

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

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

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

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

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

5
?并發(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ā)展來的。

6
?認(rèn)識兩個(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è)對象。當(dāng)線程要共享數(shù)據(jù)時(shí),就會造成 競態(tài)條件 或者 死鎖 等問題。當(dāng)然,這些問題只是可能會出現(xiàn),具體實(shí)現(xiàn)方式取決于你是否安全的使用和訪問共享對象。

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

使用獨(dú)立狀態(tài)讓我們的設(shè)計(jì)更加簡單,因?yàn)橹挥幸粋€(gè)線程能夠訪問對象,即使交換對象,也是不可變的對象。

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

并行 worker 的核心思想是,它主要有兩個(gè)進(jìn)程即代理人和工人,Delegator 負(fù)責(zé)接收來自客戶端的任務(wù)并把任務(wù)下發(fā),交給具體的 Worker 進(jìn)行處理,Worker 處理完成后把結(jié)果返回給 Delegator,在 Delegator 接收到 Worker 處理的結(jié)果后對其進(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è)好處就是,它會將一個(gè)任務(wù)拆分成多個(gè)小任務(wù),并發(fā)執(zhí)行,Delegator 在接受到 Worker 的處理結(jié)果后就會返回給 Client,整個(gè) Worker -> Delegator -> Client 的過程是異步的。
并行 Worker 的缺點(diǎn)
同樣的,并行 Worker 模式同樣會有一些隱藏的缺點(diǎn)
「共享狀態(tài)會變得很復(fù)雜」
實(shí)際的并行 Worker 要比我們圖中畫出的更復(fù)雜,主要是并行 Worker 通常會訪問內(nèi)存或共享數(shù)據(jù)庫中的某些共享數(shù)據(jù)。

這些共享狀態(tài)可能會使用一些工作隊(duì)列來保存業(yè)務(wù)數(shù)據(jù)、數(shù)據(jù)緩存、數(shù)據(jù)庫的連接池等。在線程通信中,線程需要確保共享狀態(tài)是否能夠讓其他線程共享,而不是僅僅停留在 CPU 緩存中讓自己可用,當(dāng)然這些都是程序員在設(shè)計(jì)時(shí)就需要考慮的問題。線程需要避免 競態(tài)條件,死鎖 和許多其他共享狀態(tài)造成的并發(fā)問題。
多線程在訪問共享數(shù)據(jù)時(shí),會丟失并發(fā)性,因?yàn)椴僮飨到y(tǒng)要保證只有一個(gè)線程能夠訪問數(shù)據(jù),這會導(dǎo)致共享數(shù)據(jù)的爭用和搶占。未搶占到資源的線程會 阻塞。
現(xiàn)代的非阻塞并發(fā)算法可以減少爭用提高性能,但是非阻塞算法比較難以實(shí)現(xiàn)。
可持久化的數(shù)據(jù)結(jié)構(gòu)(Persistent data structures) 是另外一個(gè)選擇??沙志没臄?shù)據(jù)結(jié)構(gòu)在修改后始終會保留先前版本。因此,如果多個(gè)線程同時(shí)修改一個(gè)可持久化的數(shù)據(jù)結(jié)構(gòu),并且一個(gè)線程對其進(jìn)行了修改,則修改的線程會獲得對新數(shù)據(jù)結(jié)構(gòu)的引用。
雖然可持久化的數(shù)據(jù)結(jié)構(gòu)是一個(gè)新的解決方法,但是這種方法實(shí)行起來卻有一些問題,比如,一個(gè)持久列表會將新元素添加到列表的開頭,并返回所添加的新元素的引用,但是其他線程仍然只持有列表中先前的第一個(gè)元素的引用,他們看不到新添加的元素。
持久化的數(shù)據(jù)結(jié)構(gòu)比如 鏈表(LinkedList) 在硬件性能上表現(xiàn)不佳。列表中的每個(gè)元素都是一個(gè)對象,這些對象散布在計(jì)算機(jī)內(nèi)存中?,F(xiàn)代 CPU 的順序訪問往往要快的多,因此使用數(shù)組等順序訪問的數(shù)據(jù)結(jié)構(gòu)則能夠獲得更高的性能。CPU 高速緩存可以將一個(gè)大的矩陣塊加載到高速緩存中,并讓 CPU 在加載后直接訪問 CPU 高速緩存中的數(shù)據(jù)。對于鏈表,將元素分散在整個(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 會將工作轉(zhuǎn)發(fā)給下一個(gè) worker。
每道程序都在自己的線程中運(yùn)行,彼此之間不會共享狀態(tài),這種模型也被稱為無共享并發(fā)模型。
使用流水線并發(fā)模型通常被設(shè)計(jì)為非阻塞I/O,也就是說,當(dāng)沒有給 worker 分配任務(wù)時(shí),worker 會做其他工作。非阻塞I/O 意味著當(dāng) worker 開始 I/O 操作,例如從網(wǎng)絡(luò)中讀取文件,worker 不會等待 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ù)通常不會按著一條裝配線流動,由于大多數(shù)程序需要做很多事情,因此需要根據(jù)完成的不同工作在不同的 worker 之間流動,如下圖所示

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

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

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

有的時(shí)候 worker 不需要明確知道接下來的 worker 是誰,他們只需要將作者寫入通道中,監(jiān)聽 Channel 的 worker 可以訂閱或者取消訂閱,這種方式降低了 worker 和 worker 之間的耦合性。
流水線設(shè)計(jì)的優(yōu)點(diǎn)
與并行設(shè)計(jì)模型相比,流水線模型具有一些優(yōu)勢,具體優(yōu)勢如下
「不會存在共享狀態(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)勢在于它能夠和硬件的工作方式相同。因?yàn)橛袪顟B(tài)的 worker 通常在 CPU 中緩存數(shù)據(jù),這樣可以更快地訪問緩存的數(shù)據(jù)。
「使任務(wù)更加有效的進(jìn)行」
可以對流水線并發(fā)模型中的任務(wù)進(jìn)行排序,一般用來日志的寫入和恢復(fù)。
流水線設(shè)計(jì)的缺點(diǎn)
流水線并發(fā)模型的缺點(diǎn)是任務(wù)會涉及多個(gè) worker,因此可能會分散在項(xiàng)目代碼的多個(gè)類中。因此很難確定每個(gè) worker 都在執(zhí)行哪個(gè)任務(wù)。流水線的代碼編寫也比較困難,設(shè)計(jì)許多嵌套回調(diào)處理程序的代碼通常被稱為 回調(diào)地獄。回調(diào)地獄很難追蹤 debug。

8
?函數(shù)性并行
函數(shù)性并行模型是最近才提出的一種并發(fā)模型,它的基本思路是使用函數(shù)調(diào)用來實(shí)現(xiàn)。消息的傳遞就相當(dāng)于是函數(shù)的調(diào)用。傳遞給函數(shù)的參數(shù)都會被拷貝,因此在函數(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)用會帶來額外的開銷。
我們之前說過,線程就是進(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 線程也是一種對象,它和其他對象一樣。Java 中的 Thread 表示線程,Thread 是 java.lang.Thread 類或其子類的實(shí)例。那么下面我們就來一起探討一下在 Java 中如何創(chuàng)建和啟動線程。

9
?創(chuà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);
????}
}
線程的主要創(chuàng)建步驟如下
定義一個(gè)線程類使其繼承 Thread 類,并重寫其中的 run 方法,run 方法內(nèi)部就是線程要完成的任務(wù),因此 run 方法也被稱為 執(zhí)行體創(chuàng)建了 Thread 的子類,上面代碼中的子類是 TJavaThread啟動方法需要注意,并不是直接調(diào)用 run方法來啟動線程,而是使用start方法來啟動線程。當(dāng)然 run 方法可以調(diào)用,這樣的話就會變成普通方法調(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 對象,調(diào)用 Thread 對象的 start() 方法為該線程執(zhí)行必須的初始化操作,然后調(diào)用 Runnable 的 run 方法,以便在這個(gè)線程中啟動任務(wù)。我們上面使用了線程的 join 方法,它用來等待線程的執(zhí)行結(jié)束,如果我們不加 join 方法,它就不會等待 tJavaThread 的執(zhí)行完畢,輸出的結(jié)果可能就不是 10000

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

