Java 提供了語言級別的線程支持,所以在 Java 中使用多線程相對于 C,C++ 來說更簡單便捷,但本文并不是介紹如何在 Java 中使用多線程來來解決諸如 Web services, Number crunching 或者 I/O processing 之類的問題。在本文中,我們將討論如何實現(xiàn)一個 Java 多線程的運行框架以及我們是如何來控制線程的并發(fā)同步以及順序執(zhí)行的。 圖 1. 線程場景 ![]() 這幅圖中節(jié)點代表一個 single Thread,邊代表執(zhí)行的步驟。 整幅圖代表的意思是,ROOT 線程執(zhí)行完畢后執(zhí)行 T1 線程,T1 執(zhí)行完畢后并發(fā)的執(zhí)行 T2 和 T3。而從 T2 和 T3 指向 T4 的兩條邊表示的是 T4 必須等 T2 和 T3 都執(zhí)行完畢以后才能開始執(zhí)行。剩下的步驟以此類推,直到 END 作為整個過程的結(jié)束。當(dāng)然,這只是個簡略的示意圖,可能面對的一個線程場景會有上百個線程。還有,你可以觀察到這整個場景只有一個入口點和一個出口點,這意味著什么?在下文中為你解釋。 這其中涉及到了 Java 線程的同步互斥機制。例如如何讓 T1 在 T2 和 T3 之前運行,如何讓 T2 和 T3 都執(zhí)行完畢之后開啟 T4 線程。
如何來描述圖 1 中所示的場景呢?可以采用 XML 的格式來描述我們的模型。我定義一個“Thread” element 來表示線程。
其中 ID 是線程的唯一標(biāo)識符,PRETHREAD 便是該線程的直接先決線程的ID,每個線程 ID 之間用逗號隔開。 在 Thread 這個 element 里面可以加入你想要該線程執(zhí)行任務(wù)的具體信息。 實際上模型的描述是解決問題非常重要的一個環(huán)節(jié),整個線程場景可以用一種一致的形式來描述,作為 Java 多線程并發(fā)控制框架引擎的輸入。也就是將線程運行的模式用 XML 來描述出來,這樣只用改動 XML 配置文件就可以更改整個線程運行的模式,不用改動任何的源代碼。
對于 Java 多線程的運行框架來說,我們將采用“外”和“內(nèi)”的兩種模式來實現(xiàn)。
圖 2. 靜態(tài)類圖 ![]() Thread 是工作線程。ThreadEntry 是 Thread 的包裝類,prerequisite 是一個 HashMap,它含有 Thread 的先決線程的狀態(tài)。如圖1中顯示的那樣,T4 的先決線程是 T2 和 T3,那么 prerequisite 中就包含 T2 和 T3 的狀態(tài)。TestScenario 中的 threadEntryList 中包含所有的 ThreadEntry。 圖 3. 線程執(zhí)行場景 ![]() TestScenario 作為主線程,作為一個“外”在的監(jiān)控者,不斷地輪詢 threadEntryList 中所有 ThreadEntry 的狀態(tài),當(dāng) ThreadEntry 接受到 isReady 的查詢后查詢自己的 prerequisite,當(dāng)其中所有的先決線程的狀態(tài)為“正常結(jié)束時”,它便返回 ready,那么 TestScenario 便會調(diào)用 ThreadEntry 的 startThread() 方法授權(quán)該 ThreadEntry 運行線程,Thread 便通過 run() 方法來真正執(zhí)行線程。并在正常執(zhí)行完畢后調(diào)用 setPreRequisteState() 方法來更新整個 Scenario,threadEntryList 中所有 ThreadEntry 中 prerequisite 里面含有該 Thread 的狀態(tài)信息為“正常結(jié)束”。 圖 4. 狀態(tài)更改的過程 ![]() 如圖 1 中所示的 T4 的先決線程為 T2 和 T3,T2 和 T3 并行執(zhí)行。如圖 4 所示,假設(shè) T2 先執(zhí)行完畢,它會調(diào)用 setPreRequisteState() 方法來更新整個 Scenario, threadEntryList 中所有 ThreadEntry 中 prerequisite 里面含有該 T2 的狀態(tài)信息為“正常結(jié)束”。此時,T4 的 prerequisite 中 T2 的狀態(tài)為“正常結(jié)束”,但是 T3 還沒有執(zhí)行完畢,所以其狀態(tài)為“未完畢”。所以 T4 的 isReady 查詢返回為 false,T4 不會執(zhí)行。只有當(dāng) T3 執(zhí)行完畢后更新狀態(tài)為“正常結(jié)束”后,T4 的狀態(tài)才為 ready,T4 才會開始運行。 其余的節(jié)點也以此類推,它們正常執(zhí)行完畢的時候會在整個的 scenario 中廣播該線程正常結(jié)束的信息,由主線程不斷地輪詢各個 ThreadEntry 的狀態(tài)來開啟各個線程。 這便是采用主控線程輪詢狀態(tài)表的方式來控制 Java 多線程運行框架的實現(xiàn)方式之一。 優(yōu)點:概念結(jié)構(gòu)清晰明了,實現(xiàn)簡單。避免采用 Java 的鎖機制,減少產(chǎn)生死鎖的幾率。當(dāng)發(fā)生異常導(dǎo)致其中某些線程不能正常執(zhí)行完畢的時候,不會產(chǎn)生掛起的線程。 缺點:采用主線程輪詢機制,耗費 CPU 時間。當(dāng)圖中的節(jié)點太多的(n>??? 而線程單個線程執(zhí)行時間比較短的時候 t<??? 需要進一步研究)時候會產(chǎn)生線程啟動的些微延遲,也就是說實時性能在極端情況下不好,當(dāng)然這可以另外寫一篇文章來專門探討。
相對于“外”-主線程輪詢機制來說,“內(nèi)”采用的是自我控制連鎖觸發(fā)機制。 圖 5. 鎖機制的靜態(tài)類圖 ![]() Thread 中的 lock 為當(dāng)前 Thread 的 lock,lockList 是一個 HashMap,持有其后繼線程的 lock 的引用,getLock 和 setLock 可以對 lockList 中的 Lock 進行操作。其中很重要的一個成員是 waitForCount,這是一個引用計數(shù)。表明當(dāng)前線程正在等待的先決線程的個數(shù),例如圖 1 中所示的 T4,在初始的情況下,他等待的先決線程是 T2 和 T3,那么它的 waitForCount 等于 2。 圖 6. 鎖機制執(zhí)行順序圖 ![]() 當(dāng)整個過程開始運行的時候,我們將所有的線程 start,但是每個線程所持的 lock 都處于 wait 狀態(tài),線程都會處于 waiting 的狀態(tài)。此時,我們將 root thread 所持有的自身的 lock notify,這樣 root thread 就會運行起來。當(dāng) root 的 run 方法執(zhí)行完畢以后。它會檢查其后續(xù)線程的 waitForCount,并將其值減一。然后再次檢查 waitForCount,如果 waitForCount 等于 0,表示該后續(xù)線程的所有先決線程都已經(jīng)執(zhí)行完畢,此時我們 notify 該線程的 lock,該后續(xù)線程便可以從 waiting 的狀態(tài)轉(zhuǎn)換成為 running 的狀態(tài)。然后這個過程連鎖遞歸的進行下去,整個過程便會執(zhí)行完畢。 我們還是以 T2,T3,T4 為例,當(dāng)進行 initThreadLock 過程的時候,我們可以知道 T4 有兩個直接先決線程 T2 和 T3,所以 T4 的 waitForCount 等于 2。我們假設(shè) T3 先執(zhí)行完畢,T2 仍然在 running 的狀態(tài),此時他會首先遍歷其所有的直接后繼線程,并將他們的 waitForCount 減去 1,此時他只有一個直接后繼線程 T4,于是 T4 的 waitForCount 減去 1 以后值變?yōu)?1,不等于 0,此時不會將 T4 的 lock notify,T4 繼續(xù) waiting。當(dāng) T2 執(zhí)行完畢之后,他會執(zhí)行與 T3 相同的步驟,此時 T4 的 waitForCount 等于 0,T2 便 notify T4 的 lock,于是 T4 從 waiting 狀態(tài)轉(zhuǎn)換成為 running 狀態(tài)。其他的節(jié)點也是相似的情況。 當(dāng)然,我們也可以將整個過程的信息放在另外的一個全局對象中,所有的線程都去查找該全局對象來獲取各自所需的信息,而不是采取這種分布式存儲的方式。 優(yōu)點:采用 wait¬ify 機制而不采用輪詢的機制,不會浪費CPU資源。執(zhí)行效率較高。而且相對于“外”-主線程輪詢的機制來說實時性更好。 缺點:采用 Java 線程 Object 的鎖機制,實現(xiàn)起來較為復(fù)雜。而且采取一種連鎖觸發(fā)的方式,如果其中某些線程異常,會導(dǎo)致所有其后繼線程的掛起而造成整個 scenario 的運行失敗。為了防止這種情況的發(fā)生,我們還必須建立一套線程監(jiān)控的機制來確保其正常運行。
下面的圖所要表達的是這樣一種遞歸迭代的概念。例如在圖1 中展示的那樣,T1 這個節(jié)點表示的是一個線程。現(xiàn)在,忘掉線程這樣一個概念,將 T1 抽象為一個過程,想象它是一個銀河系,深入到 T1 中去,它也是一個許多子過程的集合,這些子過程之間的關(guān)系模式就如圖 1 所示那樣,可以用一個圖來表示。 圖 7. 嵌套子過程 ![]() 可以想象一下這是怎樣的一個框架,具有無窮擴展性的過程框架,我們只用定義各個過程之間的關(guān)系,我們不用關(guān)心過程是怎樣運行的。事實上,可以在最終的節(jié)點上指定一個實際的工作,比如讀一個文件,或者submit一個JCL job,或者執(zhí)行一條sql statement。 其實,按照某種遍歷規(guī)則,完全可以將這種嵌套遞歸的結(jié)構(gòu)轉(zhuǎn)化成為一個一層扁平結(jié)構(gòu)的圖,而不是原來的分層的網(wǎng)狀結(jié)構(gòu),但是我們不這樣做的原因是基于以下的幾點考慮:
實際上,這是一個狀態(tài)聚集的層次控制框架,我們可以依賴此框架來執(zhí)行自主運算。我們將在其它的文章中來討論它的應(yīng)用。
本文介紹了一種 Java 多線程并發(fā)控制的框架,并給出了其兩種實現(xiàn)的模型,它們有各自的優(yōu)缺點,有各自的適用范圍。當(dāng)需要進行 Java 線程的并發(fā)控制的時候,可以作為參考。 |
|