實現 Java 多線程併發控制框架

Java 提供了語言級別的線程支持,所以在 Java 中使用多線程相對於 C,C++ 來說更簡單便捷,但本文並不是介紹如何在 Java 中使用多線程來來解決諸如 Web services, Number crunching 或者 I/O processing 之類的問題。在本文中,我們將討論如何實現一個 Java 多線程的運行框架以及我們是如何來控制線程的併發同步以及順序執行的。

陳 威 ([email protected]), 軟件工程師, IBM CSDL

2006 年 8 月 14 日

  • expand內容

所面臨的問題

圖 1. 線程場景
圖 1. 線程場景

這幅圖中節點代表一個 single Thread,邊代表執行的步驟。

整幅圖代表的意思是,ROOT 線程執行完畢後執行 T1 線程,T1 執行完畢後併發的執行 T2 和 T3。而從 T2 和 T3 指向 T4 的兩條邊表示的是 T4 必須等 T2 和 T3 都執行完畢以後才能開始執行。剩下的步驟以此類推,直到 END 作爲整個過程的結束。當然,這只是個簡略的示意圖,可能面對的一個線程場景會有上百個線程。還有,你可以觀察到這整個場景只有一個入口點和一個出口點,這意味着什麼?在下文中爲你解釋。

這其中涉及到了 Java 線程的同步互斥機制。例如如何讓 T1 在 T2 和 T3 之前運行,如何讓 T2 和 T3 都執行完畢之後開啓 T4 線程。

模型的描述

如何來描述圖 1 中所示的場景呢?可以採用 XML 的格式來描述我們的模型。我定義一個“Thread” element 來表示線程。

<ThreadList>
<Thread ID = "thread-id" PRETHREAD = "prethread1, prethread2…"></Thread>
<Thread ID = "thread-id" PRETHREAD = "prethread3, prethread4…"></Thread>
</ThreadList>

其中 ID 是線程的唯一標識符,PRETHREAD 便是該線程的直接先決線程的ID,每個線程 ID 之間用逗號隔開。

在 Thread 這個 element 裏面可以加入你想要該線程執行任務的具體信息。

實際上模型的描述是解決問題非常重要的一個環節,整個線程場景可以用一種一致的形式來描述,作爲 Java 多線程併發控制框架引擎的輸入。也就是將線程運行的模式用 XML 來描述出來,這樣只用改動 XML 配置文件就可以更改整個線程運行的模式,不用改動任何的源代碼。

兩種實現機制

對於 Java 多線程的運行框架來說,我們將採用“外”和“內”的兩種模式來實現。

“外” - 主線程輪詢

圖 2. 靜態類圖
圖 2. 靜態類圖

Thread 是工作線程。ThreadEntry 是 Thread 的包裝類,prerequisite 是一個 HashMap,它含有 Thread 的先決線程的狀態。如圖1中顯示的那樣,T4 的先決線程是 T2 和 T3,那麼 prerequisite 中就包含 T2 和 T3 的狀態。TestScenario 中的 threadEntryList 中包含所有的 ThreadEntry。

圖 3. 線程執行場景
圖 3. 線程執行場景

TestScenario 作爲主線程,作爲一個“外”在的監控者,不斷地輪詢 threadEntryList 中所有 ThreadEntry 的狀態,當 ThreadEntry 接受到 isReady 的查詢後查詢自己的 prerequisite,當其中所有的先決線程的狀態爲“正常結束時”,它便返回 ready,那麼 TestScenario 便會調用 ThreadEntry 的 startThread() 方法授權該 ThreadEntry 運行線程,Thread 便通過 run() 方法來真正執行線程。並在正常執行完畢後調用 setPreRequisteState() 方法來更新整個 Scenario,threadEntryList 中所有 ThreadEntry 中 prerequisite 裏面含有該 Thread 的狀態信息爲“正常結束”。

圖 4. 狀態更改的過程
圖 4. 狀態更改的過程

如圖 1 中所示的 T4 的先決線程爲 T2 和 T3,T2 和 T3 並行執行。如圖 4 所示,假設 T2 先執行完畢,它會調用 setPreRequisteState() 方法來更新整個 Scenario, threadEntryList 中所有 ThreadEntry 中 prerequisite 裏面含有該 T2 的狀態信息爲“正常結束”。此時,T4 的 prerequisite 中 T2 的狀態爲“正常結束”,但是 T3 還沒有執行完畢,所以其狀態爲“未完畢”。所以 T4 的 isReady 查詢返回爲 false,T4 不會執行。只有當 T3 執行完畢後更新狀態爲“正常結束”後,T4 的狀態才爲 ready,T4 纔會開始運行。

其餘的節點也以此類推,它們正常執行完畢的時候會在整個的 scenario 中廣播該線程正常結束的信息,由主線程不斷地輪詢各個 ThreadEntry 的狀態來開啓各個線程。

這便是採用主控線程輪詢狀態表的方式來控制 Java 多線程運行框架的實現方式之一。

優點:概念結構清晰明瞭,實現簡單。避免採用 Java 的鎖機制,減少產生死鎖的機率。當發生異常導致其中某些線程不能正常執行完畢的時候,不會產生掛起的線程。

缺點:採用主線程輪詢機制,耗費 CPU 時間。當圖中的節點太多的(n>??? 而線程單個線程執行時間比較短的時候 t<??? 需要進一步研究)時候會產生線程啓動的些微延遲,也就是說實時性能在極端情況下不好,當然這可以另外寫一篇文章來專門探討。

“內” - wait&notify

相對於“外”-主線程輪詢機制來說,“內”採用的是自我控制連鎖觸發機制。

圖 5. 鎖機制的靜態類圖
圖 5. 鎖機制的靜態類圖

Thread 中的 lock 爲當前 Thread 的 lock,lockList 是一個 HashMap,持有其後繼線程的 lock 的引用,getLock 和 setLock 可以對 lockList 中的 Lock 進行操作。其中很重要的一個成員是 waitForCount,這是一個引用計數。表明當前線程正在等待的先決線程的個數,例如圖 1 中所示的 T4,在初始的情況下,他等待的先決線程是 T2 和 T3,那麼它的 waitForCount 等於 2。

圖 6. 鎖機制執行順序圖
圖 6. 鎖機制執行順序圖

當整個過程開始運行的時候,我們將所有的線程 start,但是每個線程所持的 lock 都處於 wait 狀態,線程都會處於 waiting 的狀態。此時,我們將 root thread 所持有的自身的 lock notify,這樣 root thread 就會運行起來。當 root 的 run 方法執行完畢以後。它會檢查其後續線程的 waitForCount,並將其值減一。然後再次檢查 waitForCount,如果 waitForCount 等於 0,表示該後續線程的所有先決線程都已經執行完畢,此時我們 notify 該線程的 lock,該後續線程便可以從 waiting 的狀態轉換成爲 running 的狀態。然後這個過程連鎖遞歸的進行下去,整個過程便會執行完畢。

我們還是以 T2,T3,T4 爲例,當進行 initThreadLock 過程的時候,我們可以知道 T4 有兩個直接先決線程 T2 和 T3,所以 T4 的 waitForCount 等於 2。我們假設 T3 先執行完畢,T2 仍然在 running 的狀態,此時他會首先遍歷其所有的直接後繼線程,並將他們的 waitForCount 減去 1,此時他只有一個直接後繼線程 T4,於是 T4 的 waitForCount 減去 1 以後值變爲 1,不等於 0,此時不會將 T4 的 lock notify,T4 繼續 waiting。當 T2 執行完畢之後,他會執行與 T3 相同的步驟,此時 T4 的 waitForCount 等於 0,T2 便 notify T4 的 lock,於是 T4 從 waiting 狀態轉換成爲 running 狀態。其他的節點也是相似的情況。

當然,我們也可以將整個過程的信息放在另外的一個全局對象中,所有的線程都去查找該全局對象來獲取各自所需的信息,而不是採取這種分佈式存儲的方式。

優點:採用 wait&notify 機制而不採用輪詢的機制,不會浪費CPU資源。執行效率較高。而且相對於“外”-主線程輪詢的機制來說實時性更好。

缺點:採用 Java 線程 Object 的鎖機制,實現起來較爲複雜。而且採取一種連鎖觸發的方式,如果其中某些線程異常,會導致所有其後繼線程的掛起而造成整個 scenario 的運行失敗。爲了防止這種情況的發生,我們還必須建立一套線程監控的機制來確保其正常運行。

延伸

下面的圖所要表達的是這樣一種遞歸迭代的概念。例如在圖1 中展示的那樣,T1 這個節點表示的是一個線程。現在,忘掉線程這樣一個概念,將 T1 抽象爲一個過程,想象它是一個銀河系,深入到 T1 中去,它也是一個許多子過程的集合,這些子過程之間的關係模式就如圖 1 所示那樣,可以用一個圖來表示。

圖 7. 嵌套子過程
圖 7. 嵌套子過程

可以想象一下這是怎樣的一個框架,具有無窮擴展性的過程框架,我們只用定義各個過程之間的關係,我們不用關心過程是怎樣運行的。事實上,可以在最終的節點上指定一個實際的工作,比如讀一個文件,或者submit一個JCL job,或者執行一條sql statement。

其實,按照某種遍歷規則,完全可以將這種嵌套遞歸的結構轉化成爲一個一層扁平結構的圖,而不是原來的分層的網狀結構,但是我們不這樣做的原因是基於以下的幾點考慮:

  1. 如果這樣做,會導致圖節點太多,邊太多,令人眼花繚亂。
  2. 不這樣做更主要的原因是每一個場景,如圖 7 中的 T1,T13,是狀態聚集的一個單元,具有高複用性和可靠性。
  3. 框架是高度抽象的,它實際的執行可以是分佈式的,一個單元可以是一個系統,作爲和其他系統的分界標誌。

實際上,這是一個狀態聚集的層次控制框架,我們可以依賴此框架來執行自主運算。我們將在其它的文章中來討論它的應用。

總結

本文介紹了一種 Java 多線程併發控制的框架,並給出了其兩種實現的模型,它們有各自的優缺點,有各自的適用範圍。當需要進行 Java 線程的併發控制的時候,可以作爲參考。

發佈了35 篇原創文章 · 獲贊 3 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章