分布式模型的思想就是借鑒并發(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)存中?,F(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)建線程,如下示例
線程的主要?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)用了。
這樣的話,整個(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)建線程,如下示例
線程的主要?jiǎng)?chuàng)建步驟如下
- 首先定義 Runnable 接口,并重寫 Runnable 接口的 run 方法,run 方法的方法體同樣是該線程的線程執(zhí)行體。
- 創(chuàng)建線程實(shí)例,可以使用上面代碼這種簡(jiǎn)單的方式創(chuàng)建,也可以通過 new 出線程的實(shí)例來創(chuàng)建,如下所示
- 再調(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 接口,它的示例如下
我想,使用 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{
@Overridepublic 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)參考下面這篇博客
([www.cnblogs.com/xiadongqing…](
))
優(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;}
@Overridepublic void run() {Thread.currentThread().setPriority(priority);for(int i = 0;i < 100;i++){System.out.println(this);if(i % 10 == 0){Thread.yield();}}}
@Overridepublic 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{
@Overridepublic 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 {
@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r);t.setDaemon(true);return t;}}
public class DaemonFromFactory implements Runnable{
@Overridepublic 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(),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{
@Overridepublic 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{
@Overridepublic void run() {throw new RuntimeException();}
public static void main(String[] args) {try {ExecutorService service = Executors.newCachedThreadPool();service.execute(new ExceptionThread());
小伙伴們有興趣想了解內(nèi)容和更多相關(guān)學(xué)習(xí)資料的請(qǐng)點(diǎn)贊收藏+評(píng)論轉(zhuǎn)發(fā)+關(guān)注我,后面會(huì)有很多干貨。我有一些面試題、架構(gòu)、設(shè)計(jì)類資料可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,需要的話歡迎下載!私信我回復(fù)【666】即可免費(fèi)獲取
作者:Java架構(gòu)追夢(mèng)
原文出處:https://xie.infoq.cn/article/ba889a8db90d444d24c67ffbc