執行緒與同步是 Java 程式語言的核心特性,在 Java 語言規範(JLS)中有詳細定義。即時 Java 規範(RTSJ)以多種方式擴展了 JLS 的核心功能。例如,RTSJ 引入新的即時(RT)執行緒類型,這些執行緒必須遵循比一般 Java 執行緒更嚴格的排程策略;另一項創新是優先權繼承機制,定義了鎖競爭時如何管理同步。
理解優先權與優先權序列的管理,有助於掌握 RTSJ 在執行緒與同步方面的變革。優先權也是即時應用程式的重要工具。本文將透過討論如何管理執行緒優先權與優先權序列,闡述 RTSJ 的執行緒與同步機制,並探討開發、部署與執行即時應用程式(含使用 IBM WebSphere Real Time 開發的應用)時應考量的面向。
JLS 中定義的執行緒稱為一般 Java 執行緒,是 java.lang.Thread 類別的實例,擁有 1 到 10 共十個優先權等級。為適應大量執行平台,JLS 在一般 Java 執行緒的實作、排程與優先權管理上提供極大彈性。
WebSphere VMs on Linux(含 WebSphere Real Time)使用 Linux 作業系統提供的原生執行緒服務。透過理解 Linux 的執行緒與同步機制,可更深入理解 Java 的對應實作。
Linux 作業系統發展至今已提供不同使用者層級的執行緒實作。Native POSIX Thread Library(NPTL)是最新版 Linux 的策略性執行緒實作,為 WebSphere VMs 所採用。NPTL 相較前任優勢在於 POSIX 相容性與效能。編譯時可透過系統標頭檔取得 POSIX 服務;執行時則透過 libpthread.so 動態程式庫與底層 Linux 核心支援取得。Linux 核心可根據靜態控制(如執行緒優先權等級)與系統中執行緒的動態條件進行排程。
POSIX 允許建立具有不同排程策略與優先權的 POSIX 執行緒(pthreads)以滿足不同應用需求,主要有三種排程策略:
SCHED_RR 與 SCHED_FIFO 主要差別在於前者分時間片執行,後者不分。WebSphere Real Time 採用 SCHED_OTHER 與 SCHED_FIFO 策略(不使用 SCHED_RR)。
POSIX 透過 pthread_mutex 資料型別提供鎖定與同步支援。標準 Linux 版本僅支援單一預設策略,即時 Linux 版本額外支援優先權繼承鎖定策略,這將在「同步概觀」一節詳細說明。Linux 排程與鎖定均採用先進先出(FIFO)佇列管理。
RTSJ 規定一般 Java 執行緒行為與 JLS 定義一致。在 WebSphere Real Time 中,一般 Java 執行緒使用 Linux POSIX SCHED_OTHER 策略實作,主要用於編譯器、文字處理器等應用,不適合需高確定性的任務。
Linux 2.6 核心中,SCHED_OTHER 支援 40 個優先權等級,基於處理器層級管理:為緩存效能嘗試在同一處理器執行執行緒;排程主要使用處理器層級鎖而非系統層級鎖;必要時可將執行緒遷移至其他處理器以平衡負載。每個優先權等級管理「活動佇列」與「過期佇列」,邏輯上可視為每個優先權管理一個 FIFO 執行佇列。從非空佇列前端分派的執行緒擁有最高優先權,執行一段時間(時間量)後移至佇列末端並獲新時間量,如此輪替執行。
時間量長短取決於執行緒優先權,較高優先權擁有較長時間量。為防止佔用 CPU,Linux 根據執行緒是 I/O 密集或 CPU 密集等因素動態調整優先權。執行緒可透過 Thread.yield() 主動放棄時間片,或因阻塞(如等待鎖釋放)放棄控制權。
WebSphere Real Time 的 VM 未顯式對應 10 個一般 Java 優先權至 40 個 SCHED_OTHER Linux 優先權。所有一般 Java 執行緒無論其 Java 優先權如何,均被指派預設 Linux 優先權(位於 40 個等級中間)。這使一般 Java 執行緒能順利執行,無論 Linux 作何動態調整,每個執行緒最終都能獲得執行機會——前提是系統僅執行一般 Java 執行緒,而非混合即時執行緒。
注意: WebSphere Real Time VM 與非即時版 WebSphere VM 均使用 SCHED_OTHER 與預設優先權,兩者具有相似但不完全相同的排程與同步特性。類別庫、JVM、JIT 編譯器的變更,以及即時 Metronome 垃圾收集器的引入,使應用程式無法在兩種 VM 中以相同同步與效能特性執行。IBM 測試發現,同步差異導致測試程式中潛藏多年的競爭條件(即 Bug)浮現。
範例 1 展示使用一般 Java 執行緒的程式,統計兩個執行緒在五秒內的迭代次數:
class myThreadClass extends java.lang.Thread {
volatile static boolean Stop = false;
public static void main(String args[]) throws InterruptedException {
myThreadClass thread1 = new myThreadClass();
thread1.setPriority(4);
myThreadClass thread2 = new myThreadClass();
thread2.setPriority(6);
thread1.start();
thread2.start();
Thread.sleep(5*1000);
Stop = true;
}
public void run() {
System.out.println("Created thread");
int count = 0;
for (;Stop != true;) {
count++;
Thread.yield();
}
System.out.println("Thread terminates. Loop count is " + count);
}
}
程式包含三個使用者執行緒:啟動過程隱含建立的主執行緒(執行 main())、優先權 4 的執行緒、優先權 6 的執行緒。主執行緒睡眠 5 秒後通知其他執行緒結束;兩個子執行緒各自迴圈計數並在每次迭代呼叫 Thread.yield()。
在單處理器或閒置多處理器上執行,兩個執行緒的迭代計數大致相同(如 540084 與 540083)。因兩者在 SCHED_OTHER 策略下獲指派相同預設優先權,獲得相同時時間片,執行相同程式碼故動態優先權調整相似,在同一執行佇列輪替。優先權 4 的執行緒因先啟動,在五秒內稍多獲得一些時間,計數略大。若移除 Thread.yield(),計數仍相近但不完全相同。
即時執行緒是 javax.realtime.RealtimeThread 的實例。RTSJ 要求實作至少提供 28 個連續的即時優先權,稱為即時優先權。規範未指定起始值,僅規定高於一般 Java 執行緒最高優先權(10)。為可攜性,應用程式碼應使用 PriorityScheduler 類別的 getPriorityMin() 與 getPriorityMax() 方法確定可用即時優先權範圍。
JLS 執行緒排程不精確且僅提供 10 個優先權值。Linux POSIX SCHED_OTHER 滿足各類應用需求,但具備一些不佳特性:動態優先權調整與時間片劃分可能在不可預測時間發生;SCHED_OTHER 的 40 個優先權等級其實不多,部分已被一般 Java 應用與動態調整佔用;JVM 內部執行緒(如垃圾收集)也需優先權。缺乏確定性、需要更高優先權等級、與現有應用相容等因素,催生了擴展需求,RTSJ javax.realtime 套件提供新排程功能。WebSphere Real Time 中以 Linux SCHED_FIFO 策略滿足 RTSJ 排程需求。
WebSphere Real Time 支援 28 個即時 Java 優先權(範圍 11-38),可透過 PriorityScheduler API 取得。RTSJ 將即時優先權視為執行時系統邏輯實作的優先權,為每個即時優先權維護獨立佇列。排程器必須從非空最高優先權佇列頭部分派。若所有佇列無即時優先權執行緒,則依 JLS 排程一般 Java 執行緒。
擁有即時優先權的執行緒可持續執行直到阻塞、主動讓步、或被更高即時優先權執行緒搶佔。主動讓步的執行緒會被置於其佇列末端。RTSJ 要求此類排程在固定時間內完成,不隨當前執行即時執行緒數量等因素變化。RTSJ 1.02 版僅對單處理器系統規定此規則;多處理器系統排程行為未作要求。
Linux SCHED_FIFO 策略滿足所有 RTSJ 排程需求:提供 99 個優先權等級(遠多於 SCHED_OTHER 的 40 個);不分時間片;不動態調整即時執行緒優先權(除非透過優先權繼承鎖定策略)。Linux 為即時與一般執行緒提供固定時間排程。多處理器系統中,Linux 嘗試模擬分派至可用處理器的單一全域即時執行緒佇列行為,這最貼近 RTSJ 精神,但與一般 Java 執行緒的 SCHED_OTHER 策略不同。
範例 2 修改範例 1 使用即時執行緒而非一般執行緒:
import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
volatile static boolean Stop = false;
public static void main(String args[]) throws InterruptedException {
myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
thread1.start();
thread2.start();
Thread.sleep(5*1000);
Stop = true;
}
public void run() {
System.out.println("Created thread");
int count = 0;
for (;Stop != true;) {
count++;
// Thread.yield(); // 註解掉
}
System.out.println("Thread terminates. Loop count is " + count);
}
}
此程式在單處理器環境永遠不會結束,僅列印 Created thread。原因:主執行緒仍為一般 Java 執行緒(SCHED_OTHER),啟動第一個即時執行緒後即被搶佔,該即時執行緒不受時間量限制且不阻塞,將無限期執行。主執行緒被搶佔後永無執行機會,無法啟動第二個即時執行緒,也無法設定 Stop = true 結束迴圈。Thread.yield() 也無法讓出 CPU,因讓步邏輯將即時執行緒置於其佇列末端,但排程器會再次選擇此最高優先權執行緒。
雙處理器系統同樣失敗,會列印兩行 Created thread 後停滯:主執行緒建立兩個即時執行緒後被搶佔,兩個即時執行緒佔據兩個處理器永不阻塞,主執行緒永無機會設定結束旗標。僅在三個以上處理器系統上才能正常完成。
範例 3 修正程式使其在單處理器正確執行:將 main() 邏輯移至一個優先權為 8 的「主即時執行緒」中,此優先權高於其建立的兩個即時執行緒(優先權 4 與 6)。擁有最高即時優先權使主即時執行緒能成功建立兩個執行緒,並在 5 秒睡眠醒來時搶佔當前執行緒。
import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
volatile static boolean Stop = false;
static class myRealtimeStartup extends javax.realtime.RealtimeThread {
public void run() {
myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
thread1.start();
thread2.start();
try { Thread.sleep(5*1000); } catch (InterruptedException e) {}
myRealtimeThreadClass.Stop = true;
}
}
public static void main(String args[]) {
myRealtimeStartup startThr = new myRealtimeStartup();
startThr.setPriority(PriorityScheduler.getMinPriority(null)+ 8);
startThr.start();
}
public void run() {
System.out.println("Created thread");
int count = 0;
for (;Stop != true;) { count++; }
System.out.println("Thread terminates. Loop count is " + count);
}
}
單處理器執行輸出:
Created thread
Thread terminates. Loop count is 32767955
Created thread
Thread terminates. Loop count is 0
解析:主即時執行緒(優先權 8)執行直到 Thread.sleep() 阻塞。睡眠期間,優先權 6 的執行緒獲得 CPU 並持續執行,直到主執行緒醒來設定 Stop = true 並結束。隨後優先權 6 執行緒執行一輪迴圈檢查旗標並結束(計數非零)。最後優先權 4 執行緒獲得 CPU,但旗標已為真,迴圈零次執行即結束(計數為零)。
RTSJ 提供工具建立在特定或相對時間啟動的即時執行緒:可建立在指定間隔或週期執行邏輯的執行緒;可定義未在指定期限完成時觸發 AsynchronousEventHandler(AEH)的執行緒;可限制執行緒可使用的記憶體類型與數量,超限拋出 OutOfMemoryError。這些工具僅對即時執行緒可用,一般 Java 執行緒不支援。
Thread.interrupt() 與未決異常即時執行緒擴展了 Thread.interrupt() 行為:除像 JLS 所述中斷阻塞程序外,若方法宣告加入 Throws AsynchronouslyInterruptedException 子句標記為可中斷,也會引發此異常。此異常會「糾纏」執行緒,使用者必須顯式清除;否則異常持續「未決」。若執行緒以「常規」方式結束但未清除異常,在執行緒池應用中會導致問題:重新分配帶有未決異常的池化執行緒時,可能欺騙性地拋出異常。池化程式碼必須顯式清除異常。
主執行緒通常為一般 Java 執行緒而非即時執行緒。第一個即時執行緒總由一般執行緒建立。若可用處理器不足以同時執行即時與一般執行緒,即時執行緒會立即搶佔一般執行緒,阻止其繼續建立即時執行緒或執行初始化邏輯。解決方法是從高優先權即時執行緒執行應用程式初始化。執行執行緒池化與排程的應用程式或程式庫可能需要此技術:排程邏輯應在高優先權執行緒中執行,以防止入隊/出隊問題。
一般 Java 執行緒按時間量執行,動態優先權根據 CPU 使用率調整,確保所有執行緒最終執行。反之,即時執行緒不受時間量限制,排程器不依 CPU 使用率動態調整優先權。這種差異使「失控即時執行緒」成為可能:佔用系統、阻止其他應用執行、甚至阻止使用者登入。開發測試期間可限制進程可用 CPU 數量緩解:Linux 會在進程耗盡 CPU 限制時終止失控執行緒。此外,監控系統狀態或提供登入的程式應以高即時優先權執行,以便搶佔問題執行緒。
Linux POSIX SCHED_FIFO 提供 1-99 共 99 個即時優先權。WebSphere VM 使用 11-89 範圍,子集實作 28 個 RTSJ 優先權。28 個即時 Java 優先權對映至此範圍的 POSIX 系統優先權(IBM 文件有詳細說明),但應用程式碼不應依賴此對映,僅應依賴 Java 層面 28 個即時優先權的相對順序,以便 JVM 未來重新對映並改進。若某些連接埠監控程式或即時進程需優先權超出 WebSphere Real Time 範圍,可使用 SCHED_FIFO 優先權 1 或 90 實作。
JNI AttachThread()Java Native Interface (JNI) 允許用 JNI AttachThread() 將 C 程式碼建立的執行緒加入 JVM,但 RTSJ 未對 JNI 介面變更以支援加入即時執行緒。應用程式應避免用 C 程式碼建立擬加入 JVM 的 POSIX 即時執行緒,而應在 Java 語言中建立此類即時執行緒。
執行緒可衍生新進程。Linux 上,衍生進程的主執行緒繼承父執行緒優先權。若衍生進程為 JVM,其主執行緒將帶即時優先權建立,這與一般 Java 執行緒順序衝突(主執行緒排程優先權應低於即時執行緒)。為防止此情況,JVM 強制主執行緒擁有非即時優先權(即 SCHED_OTHER 策略)。
Thread.yield()Thread.yield() 僅讓步給相同優先權執行緒,決不讓步給較高或較低優先權執行緒。在使用多個即時優先權的即時應用中使用 Thread.yield() 可能有問題,除非完全必要否則應避免。
NoHeapRealtimeThreadsjavax.realtime.NoHeapRealtimeThread (NHRT) 是 RTSJ 新執行緒類型,繼承自 RealtimeThread。NHRT 擁有相同排程特性,但不會被 GC 搶佔,且無法讀寫 Java 堆。NHRT 是 RTSJ 重要面向,本系列後續文章將討論。
AsynchronousEventHandlersAsynchronousEventHandler (AEH) 是 RTSJ 新增程式,可視為事件發生時執行的一種即時執行緒。可設定 AEH 在特定或關聯時間觸發。AEH 同樣擁有即時執行緒的排程特性,且有堆與非堆兩種風格。
許多 Java 應用直接使用執行緒特性,或使用涉及多執行緒的程式庫。多執行緒程式設計的主要考量是確保程式在多執行緒系統中正確(執行緒安全)執行。需序列化存取由多執行緒共享的資料,使用鎖或原子機器操作等同步原語。即時應用程式設計師常面臨使程式在時間限制內執行的挑戰,可能需了解組件實作細節、含意與效能屬性。
Java 提供三種核心同步原語:
Object.wait():釋放物件鎖,執行緒等待Object.notify() / notifyAll():喚醒等待執行緒執行 wait() 與 notify() 的執行緒必須已鎖定物件。當執行緒嘗試鎖定已被其他執行緒鎖定的物件時發生鎖競爭。未獲得鎖的執行緒被置於物件鎖競爭者邏輯佇列;多執行緒對同一物件執行 Object.wait() 形成等待者佇列。JLS 未指定佇列管理方式,但 RTSJ 規定了此行為。
RTSJ 原則:所有執行緒佇列均為 FIFO 且基於優先權。基於優先權的 FIFO 行為——接著執行最高優先權執行緒——適用於鎖競爭者與等待者佇列。鎖競爭者的 FIFO 基於優先權佇列類似執行佇列;釋放鎖後,系統從競爭者最高優先權佇列前端選擇執行緒嘗試鎖定物件;同理,notify() 後,等待者最高優先權佇列前端執行緒解除阻塞。鎖釋放或 notify() 操作類似排程分派,均作用於最高優先權佇列頭部執行緒。
支援基於優先權同步需修改即時 Linux,亦需修改 WebSphere Real Time VM,委托 Linux 在 notify() 時選擇解除阻塞的執行緒。
優先權反轉:高優先權執行緒所需鎖被低優先權執行緒持有。中等優先權執行緒可能搶佔持有鎖的低優先權執行緒並優先執行,導致高優先權執行緒延遲,可能錯過關鍵時限。
優先權繼承:避免優先權反轉的技術,由 RTSJ 規定。思想是發生鎖競爭時,鎖持有者優先權提升至欲獲取鎖的執行緒優先權;釋放鎖時優先權「降」回基本優先權。此機制使低優先權執行緒在持有鎖期間以高優先權執行,釋放後高優先權執行緒立即鎖定並繼續,中等優先權執行緒無法延遲高優先權執行緒。

可能發生鏈狀情況:高優先權執行緒嘗試獲取低優先權執行緒持有的鎖,而低優先權執行緒自身又被另一執行緒持有的另一鎖阻塞。此時低優先權執行緒與另一執行緒均會被提升優先權,即優先權繼承需對一組執行緒進行提升與降級。
優先權繼承透過 Linux 核心功能提供,經 POSIX 鎖定服務導出至使用者空間。完全位於使用者空間的方案不滿意,因為:Linux 核心可能被搶佔且常發生優先權反轉,某些系統鎖也需優先權繼承;使用者空間方案導致難解決的競態條件;優先權提升總需核心呼叫。
POSIX 鎖型別為 pthread_mutex。建立時指定互斥鎖屬性實作優先權繼承協定。Linux 在無鎖競爭時在使用者空間執行所有鎖定;發生競爭時在核心空間進行優先權提升與同步佇列管理。
WebSphere VM 使用 POSIX 鎖定 API 實作核心 Java 同步原語以支援優先權繼承。使用者層級 C 程式碼也可使用這些 POSIX 服務。Java 層級鎖定操作分配唯一 pthread_mutex 並用原子機器指令綁定至 Java 物件。解鎖時若無競爭,用原子操作解除綁定;有競爭則觸發 Linux 核心優先權繼承支援。
為最小化互斥鎖分配與鎖定時間,JVM 管理全域鎖快取與單執行緒鎖快取,每個快取包含未分配的 pthread_mutex。執行緒專用快取的互斥鎖從全域快取取得,放入前預先鎖定。非競爭解鎖將鎖定的互斥鎖歸還執行緒快取。假設非競爭鎖定為常態,透過重用預先鎖定的互斥鎖減少並分攤 POSIX 層級鎖定開銷。
JVM 內部擁有基於優先權繼承的鎖,用於序列化關鍵資源(如執行緒列表、全域鎖快取)存取,持有時間極短。
即時執行緒可能被一般 Java 執行緒持有的鎖阻塞。此時優先權繼承介入,一般執行緒優先權提升至即時執行緒優先權並保持持有鎖期間。一般執行緒繼承即時執行緒所有排程特性:按 SCHED_FIFO 執行(不分時間片)、在提升後的即時執行佇列中排程與讓步。釋放鎖後恢復 SCHED_OTHER。若範例 1 的兩個執行緒在持有即時執行緒所需鎖時運行,程式將不結束,出現「有問題的即時執行緒程式碼」一節所述問題。因此在即時 JVM 中執行自旋迴圈與讓步並不明智。
NHRT 可能在即時執行緒(或對應一般執行緒)持有的鎖處阻塞。雖即時執行緒持有鎖,GC 可能搶佔即時執行緒間接搶佔 NHRT。NHRT 必須等待即時執行緒不再被 GC 搶佔並釋放鎖才能執行。若 NHRT 功能有嚴格時間要求,GC 搶佔將是嚴重問題。WebSphere Real Time 確定性垃圾收集器將暫停時間控制在 1 毫秒以下,使 NHRT 搶佔更具確定性。若無法容忍此類暫停,可避免 NHRT 與即時執行緒共享鎖;若強制用鎖,可考慮使用專屬資源與鎖(如分離的池與池鎖)。
javax.realtime 套件提供:
WaitFreeReadQueue:主要用於從即時執行緒傳遞物件至 NHRTWaitFreeWriteQueue:主要用於從 NHRT 傳遞物件至即時執行緒這些類別保證即時執行緒執行無等待操作時不持有 NHRT 所需鎖。
javax.realtime 套件中的同步某些 javax.realtime 方法故意未實作同步,因即使鎖無競爭,同步也有系統開銷。若需同步,呼叫端負責在同步方法或區塊中封裝所需的 javax.realtime 方法。程式設計師使用 java.realtime 套件方法時必須考量添加同步。
相反,java.util.Vector 等核心 JLS 服務已實作同步。某些核心 JLS 服務執行內部鎖定序列化共享資源。因這種同步,使用核心 JLS 服務時必須謹慎,避免 GC 搶佔 NHRT 問題(參見上節)。
非即時應用標準檢查顯示鎖定主要為無競爭。無競爭鎖定同樣被認為是即時應用中的主要類型,特別是重用現有組件或程式庫時。若已知鎖定無競爭但難以避免或移除同步指令,付出小量確定開銷是明智的。
無競爭鎖定涉及一些設定與一個原子機器指令;解鎖涉及一個原子指令。鎖定設定涉及分配預先鎖定的互斥鎖,這是無競爭鎖定中最大的可變開銷。RealtimeSystem.setMaximumConcurrentLocks() 可協助控制此開銷:使 VM 預先分配指定數量互斥鎖至全域鎖快取,快取內容提供給單執行緒快取。透過此 API 可降低嚴格時間要求程式碼區域發生鎖定初始化的機率。RealtimeSystem.getMaximumConcurrentLocks() 可協助決定呼叫參數,但注意它提供的是呼叫時的鎖使用量而非最高水位標記。未來 RTSJ 版本可能提供最高水位標記 API。勿為 numLocks 指定過大值,因呼叫可能耗費過量時間與記憶體建立鎖。此 API 定義為 JVM 專用,其他 JVM 可能忽略或行為不同。
執行緒可同時持有多個鎖並按特定順序獲取,形成鎖層級結構。優先權繼承可提升並降低一組執行緒優先權,組中執行緒數不超過系統最深鎖層級結構深度。保持較淺鎖層級結構可鎖定最少物件,影響需調整優先權的最大執行緒數量。
Object.wait(long timeout, int nanos) 為等待操作提供奈秒粒度。HighResolutionTime.waitForObject() API 類似 Object.wait() 並提供可用奈秒粒度指定的相對與絕對時間。WebSphere Real Time 中兩者均使用底層 POSIX 鎖定等待服務實作,底層服務最多提供微秒粒度。如需可攜性,可用 javax.realtime 套件 Clock 類別的 getResolution() 方法取得執行平台解析度。
RTSJ 透過 javax.realtime 套件中的新即時類別與 API,擴展並強化了 Java 程式設計師的執行緒與同步功能。WebSphere Real Time 透過即時 Linux 核心、POSIX 執行緒修改與 JVM 修改實作這些功能。您現對 RTSJ 執行緒與同步有更深入理解,這將助您在撰寫與部署即時應用程式時避免問題。