程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 實時Java,第3部分 - 線程化和同步

實時Java,第3部分 - 線程化和同步

編輯:關於JAVA

線程化和同步是 Java 編程語言的核心特性,Java 語言規范(JLS)中對二者作出了描述。RTSJ 用多種方式擴展了 JLS 的核心功能。(參見 參考資料 中關於 JLS 和 RTSJ 的鏈接。)例如,RTSJ 引入了一些新的實時(RT)線程類型,它們必須遵守比普通 Java 線程更加嚴格的調度策略。另一個例子是優先級繼承,它是一種鎖定策略,定義了鎖競爭時如何管理鎖同步。

理解對優先級和優先級序列的管理有助於理解 RTSJ 針對線程化和同步所作的更改。優先級也是 RT 應用程序使用的一種重要工具。本文通過討論如何管理線程優先級和優先級序列來描述 RTSJ 線程化和同步。討論了開發、部署和執行 RT 應用程序(包括使用 IBM WebSphere® Real Time 開發的應用程序,參見 參考資料)時應該考慮的一些方面。

理解普通的 Java 線程

JLS 中定義的線程稱為普通 Java 線程。普通 Java 線程是 java.lang.Thread 類的一個實例,該類擁有從 1 到 10 的 10 個優先級別。為了適應大量的執行平台,JLS 在如何實現、調度和管理普通 Java 線程的優先級方面提供了很大的靈活性。

WebSphere VMs on Linux®(包括 WebSphere Real Time)使用 Linux 操作系統提供的本地線程化服務。您可以通過理解 Linux 的線程化和同步來學習 Java 的線程化和同步。

Linux 線程化和同步

Linux 操作系統發展至今已經提供了不同用戶級別的線程化實現。Native POSIX Thread Library(NPTL)(參見 參考資料)是 Linux 最新版本的戰略性線程化實現,由 WebSphere VMs 所使用。NPTL 與它的前任相比優勢在於 POSIX 兼容性和性能。在編譯時可通過系統的頭文件獲取 POSIX 服務。可在運行時通過 libpthread.so 動態庫和底層 Linux 核心支持獲取 POSIX 服務。Linux 核心可以根據靜態控制(如線程優先級級別)和系統中執行的線程的某些動態條件下來執行線程調度。

POSIX 允許您創建具有不同線程調度策略和優先級的 POSIX 線程(pthreads)以滿足不同應用程序的需求。下面是三種此類的調度策略:

SCHED_OTHER

SCHED_FIFO

SCHED_RR

SCHED_OTHER 策略用於傳統用戶任務,如程序開發工具、辦公應用程序和 Web 浏覽器。 SCHED_RR 和 SCHED_FIFO 主要用於具有更高的確定性和時限需求的應用程序。SCHED_RR 和 SCHED_FIFO 之間的主要區別是 SCHED_RR 分時間片 執行線程,而 SCHED_FIFO 則不是這樣。SCHED_OTHER 和 SCHED_FIFO 策略用於 WebSphere Real Time,並在下面作出了更加詳細的描述。(我們不介紹 SCHED_RR 策略,WebSphere Real Time 沒有使用它。)

POSIX 通過 pthread_mutex 數據類型提供鎖定和同步支持。pthread_mutex 可以使用不同的鎖定策略創建。當多個線程需要同時獲取同一個鎖的時候,鎖定策略常常會影響執行行為。標准的 Linux 版本支持單個的默認策略,而 RT Linux 版本還支持優先級繼承鎖定策略。我們將在本文的 同步概述 一節對優先級繼承策略作更詳細的描述。

Linux 調度和鎖定用來管理先進先出(FIFO)隊列。

普通 Java 線程的線程調度

RTSJ 指出普通 Java 線程的行為跟 JLS 中定義的相同。在 WebSphere Real Time 中,普通 Java 線程使用 Linux 的 POSIX SCHED_OTHER 調度策略來實現。SCHED_OTHER 策略主要用於編譯器和字處理程序之類的應用程序,不能用於需要更高確定性的任務。

在 2.6 Linux 內核中,SCHED_OTHER 策略支持 40 個優先級級別。這 40 個優先級級別基於處理器級別來管理,就是說:

出於緩存性能的原因,Linux 嘗試在同一個處理程序中執行線程。

線程調度主要使用處理器級別的鎖而不是系統級別的鎖。

如有需要,Linux 可將線程從一個處理程序遷移到另一個處理程序以平衡工作量。

在(40 個中的)每個優先級級別中,Linux 管理活動隊列 和過期隊列。每個隊列包含一個線程鏈表(或者為空)。使用活動和過期隊列出於以下目的:效率、負載平衡和其他一些目的。邏輯上可將系統看作:為(40 個中的)每個優先級管理一個 FIFO 序列,稱為運行隊列。一個從非空運行隊列的前端分派的線程具有最高的優先級。該線程從隊列中移除並執行一段時間(稱作:時間量 或時間片)。當一個執行線程超過 它的時間量時,它的優先級被放在運行隊列的後端並給它指定了新的時間量。通過從隊列的前端分派線程和在隊列的後端放置過期的線程,程序在一個優先級中輪替執行。

為線程提供的時間量取決於給線程指定的優先級。指定了較高優先級的線程擁有較長的執行時間量。為了防止線程霸占 CPU,Linux 根據一些因素(如線程是 I/O 限制還是 CPU 限制)動態提高或降低線程的優先級。線程可以通過讓步(如調用 Thread.yield())自願地放棄它的時間片,或通過阻塞放棄控制權,在阻塞處等待事件發生。釋放鎖可以觸發一個這類的事件。

WebSphere Real Time 中的 VM 沒有顯式地指定跨越 40 個 SCHED_OTHER Linux 線程優先級的 10 個普通 Java 線程優先級。所有的普通 Java 線程,不論其 Java 優先級如何,都被指定為默認的 Linux 優先級。默認的 Linux 優先級處於 40 個 SCHED_OTHER 優先級的中間位置。通過使用默認優先級,普通 Java 線程可以順利地執行,即不論 Linux 可能作出何種動態優先級調整,運行隊列中的每個普通 Java 線程都能最終得到執行。這裡假設的是只執行普通 Java 線程的系統而不是其他系統,比如執行 RT 線程的系統。

注意:WebSphere Real Time 中的 VM 和 WebSphere VM 的非 RT 版本都使用 SCHED_OTHER 策略並針對普通 Java 線程使用默認優先級指定。通過使用相同的策略,這兩種 JVM 具有相似但不相同的線程調度和同步特征。WebSphere Real Time 類庫中的更改、JVM 中的更改和為支持 RTSJ 而在 JIT 編譯器中作出的更改,以及 RT Metronome 垃圾收集器的引入(參見 參考資料)使應用程序不可能在兩種虛擬機中以相同的同步和性能特征運行。在 IBM WebSphere Real Time 測試期間,在測試程序中,同步差異使競爭條件(換言之,bug)浮出了水面,而這些測試程序已經在其他 JVM 上運行了很多年。

使用普通 Java 線程的代碼示例

清單 1 展示了一個使用普通 Java 線程的程序,確定了兩個線程中的每一個在五秒的時間間隔內在一個循環中執行的迭代次數:

清單 1. 普通 Java 線程

class myThreadClass extends java.lang.Thread {
  volatile static boolean Stop = false;
  // Primordial thread executes main()
  public static void main(String args[]) throws InterruptedException {
    // Create and start 2 threads
    myThreadClass thread1 = new myThreadClass();
    thread1.setPriority(4);  // 1st thread at 4th non-RT priority
    myThreadClass thread2 = new myThreadClass();
    thread2.setPriority(6);  // 2nd thread at 6th non-RT priority
    thread1.start();      // start 1st thread to execute run()
    thread2.start();      // start 2nd thread to execute run()
    // Sleep for 5 seconds, then tell the threads to terminate
    Thread.sleep(5*1000);
    Stop = true;
  }
  public void run() { // Created threads execute this method
    System.out.println("Created thread");
    int count = 0;
    for (;Stop != true;) {  // continue until asked to stop
     count++;
     Thread.yield();  // yield to other thread
    }
    System.out.println("Thread terminates. Loop count is " + count);
  }
}

清單 1 中的程序具有三個普通 Java 線程的用戶線程:

原始線程:

它是啟動過程中隱式創建的主線程,執行 main() 方法。

main() 創建了兩個普通 Java 線程:一個線程的優先級為 4 而另一個線程的優先級為 6。

主線程通過調用 Thread.sleep() 方法休眠五秒鐘來達到故意阻塞自身的目的。

休眠五秒鐘後,此線程指示其他兩個線程結束。

優先級為 4 的線程:

此線程由原始線程創建,後者執行包含 for 循環的 run() 方法。

該線程:

在每次循環迭代中增加一個計數。

通過調用 Thread.yield() 方法自願放棄它的時間片。

在主線程發出請求時結束。結束前打印循環計數。

優先級為 6 的線程:此線程執行的動作與優先級為 4 的線程相同。

如果此程序在單處理器或卸載的多處理器上運行,則每個線程打印的 for 循環迭代計數大致相同。在一次運行中,程序將打印:

Created thread
Created thread
Thread terminates. Loop count is 540084
Thread terminates. Loop count is 540083

如果刪除對 Thread.yield() 的調用,則兩個線程的循環計數可能相近,但絕不可能相同。在 SCHED_OTHER 策略中為這兩個線程都指定了相同的默認優先級。因此給兩個線程分配了相同的時間片執行。因為線程執行的是相同的代碼,所以它們應作出類似的動態優先級調整並在相同的運行隊列中輪替執行。但是由於首先執行優先級為 4 的線程,因此在五秒鐘的執行時間間隔中,它分得的時間稍多一些並且打印的循環計數也稍大一些。

理解 RT 線程

RT 線程是 javax.realtime.RealtimeThread 的一個實例。RTSJ 要求規范的實現必須為 RT 線程提供至少 28 個連續的優先級。這些優先級被稱作實時優先級。規范中並沒有指定 RT 優先級范圍的開始值,除非其優先級高於 10 —— 普通 Java 線程的最高優先級值。出於可移植性的原因,應用程序代碼應使用新的 PriorityScheduler 類的 getPriorityMin() 和 getPriorityMax() 方法來確定可用的 RT 優先級值的范圍。

對 RT 線程的推動

JLS 中的線程調度並不精確而且只提供了 10 個優先級值。由 Linux 實現的 POSIX SCHED_OTHER 策略滿足了各種應用程序的需要。但是 SCHED_OTHER 策略具有一些不好的特性。動態優先級調整和時間片劃分可能在不可預測的時間內發生。SCHED_OTHER 優先級的值(40)其實並不算大,其中一部分已經被使用普通 Java 線程的應用程序和動態優先級調整利用了。JVM 還需要對內部線程使用優先級以達到一些特殊目的,比如垃圾收集(GC)。

缺少確定性、需要更高的優先級級別以及要求與現有應用程序兼容,這些因素引發了對擴展的需求,這將為 Java 程序員提供新的調度功能。RTSJ 中描述的 javax.realtime 包中的類提供了這些功能。在 WebSphere Real Time 中,Linux SCHED_FIFO 調度策略滿足了 RTSJ 調度需求。

RT Java 線程的線程調度

在 WebSphere Real Time 中,支持 28 個 RT Java 優先級,其范圍為 11 到 38。PriorityScheduler 類的 API 應用於檢索這個范圍。本節描述了比 RTSJ 更多的線程調度細節以及 Linux SCHED_FIFO 策略的一些方面,已經超出了 RTSJ 的需求。

RTSJ 將 RT 優先級視作由運行時系統在邏輯上實現的優先級,該系統為每個 RT 優先級保持一個獨立隊列。線程調度程序必須從非空的最高優先級隊列的頭部開始調度。注意:如果所有隊列中的線程都不具有 RT 優先級,則調度一個普通 Java 線程按 JLS 中的描述執行(參見 普通 Java 線程的線程調度)。

具有 RT 優先級的調度線程可以一直執行直至阻塞,通過讓步自願放棄控制權,或被具有更高 RT 優先級的線程搶占。具有 RT 優先級並自願讓步的線程的優先級被置於隊列的後端。RTSJ 還要求此類調度在不變的時間內進行,並且不能隨某些因素變化(如當前執行的 RT 線程的數量)。RTSJ 的 1.02 版本對單處理器系統應用了這些規則;RTSJ 對於多處理器系統上的調度如何運作未作要求。

Linux 為所有適當的 RTSJ 調度需求提供了 SCHED_FIFO 策略。SCHED_FIFO 策略用於 RT 而不用於用戶任務。SCHED_FIFO 與 SCHED_OTHER 策略的區別在於前者提供了 99 個優先級級別。SCHED_FIFO 不為線程分時間片。同樣,SCHED_FIFO 策略也不動態調整 RT 線程的優先級,除非通過優先級繼承鎖定策略(同步概述 一節對此作出了描述)。由於優先級繼承的原因,RTSJ 需要使用優先級調整。

Linux 為 RT 線程和普通 Java 線程提供不變時間調度。在多處理器系統中,Linux 試圖模擬分派到可用處理器的單個全局 RT 線程隊列的行為。這與 RTSJ 的精神最為接近,但確實與用於普通 Java 線程的 SCHED_OTHER 策略不同。

使用 RT 線程的有問題的代碼示例

清單 2 修改 清單 1 中的代碼來創建 RT 線程而不是普通 Java 線程。使用 java.realtime.RealtimeThread 而不是 java.lang.Thread 指出了其中的區別。第一個線程創建於第 4 RT 優先級而第二個線程創建於第 6 RT 優先級,與 getPriorityMin() 方法確定的相同。

清單 2. RT 線程

import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
  volatile static boolean Stop = false;
  // Primordial thread executes main()
  public static void main(String args[]) throws InterruptedException {
    // Create and start 2 threads
    myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
    // want 1st thread at 4th real-time priority
    thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
    myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
    // want 2nd thread at 6th real-time priority
    thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
    thread1.start();      // start 1st thread to execute run()
    thread2.start();      // start 2nd thread to execute run()
    // Sleep for 5 seconds, then tell the threads to terminate
    Thread.sleep(5*1000);
    Stop = true;
  }
  public void run() { // Created threads execute this method
    System.out.println("Created thread");
    int count = 0;
    for (;Stop != true;) {  // continue until asked to stop
     count++;
     // Thread.yield();  // yield to other thread
    }
    System.out.println("Thread terminates. Loop count is " + count);
  }
}

清單 2 中修改後的代碼存在一些問題。如果程序在單處理器環境中運行,則它永遠不會結束並且只能打印以下內容:

Created thread

出現這樣的結果可以用 RT 線程調度的行為來解釋。原始線程仍然是一個普通 Java 線程並利用非 RT(SCHED_OTHER)策略運行。只要原始線程啟動第一個 RT 線程,RT 線程就搶占原始線程並且 RT 線程將會不確定地運行,因為它不受時間量和線程阻塞的限制。原始線程被搶占後,就再也不允許執行,因此再也不會啟動第二個 RT 線程。Thread.yield() 對允許原始線程執行反而不起作用 —— 因為讓步邏輯將 RT 線程置於其運行隊列的末端 —— 但是線程調度程序將再次調度這個線程,因為它是運行隊列前端的具有最高優先級的線程。

該程序在雙處理器系統中同樣會失敗。它將打印以下內容:

Created thread
Created thread

允許使用原始線程創建這兩個 RT 線程。但是創建第二個線程後,原始線程被搶占並且再也不允許告知線程結束,因為兩個 RT 線程在兩個處理器上執行而且永遠不會阻塞。

在帶有三個或更多處理器的系統上,程序運行至完成並生成一個結果。

單處理器上運行的 RT 代碼示例

清單 3 顯示了修改後能在單處理器系統中正確運行的代碼。main() 方法的邏輯被移到了一個具有第 8 RT 優先級的 “main” RT 線程中。這個優先級比主 RT 線程創建的兩個其他 RT 線程的優先級都要高。擁有最高的 RT 優先級使這個主 RT 線程能夠成功地創建兩個 RT 線程,並且還允許它從五秒鐘的休眠中蘇醒時能夠搶占當前運行的線程。

清單 3. 修改後的 RT 線程示例

import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
  volatile static boolean Stop = false;
  static class myRealtimeStartup extends javax.realtime.RealtimeThread {
  public void run() {
    // Create and start 2 threads
    myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
    // want 1st thread at 4th real-time priority
    thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
    myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
    // want 1st thread at 6th real-time priority
    thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
    thread1.start();      // start 1st thread to execute run()
    thread2.start();      // start 2nd thread to execute run()
    // Sleep for 5 seconds, then tell the threads to terminate
    try {
             Thread.sleep(5*1000);
    } catch (InterruptedException e) {
    }
    myRealtimeThreadClass.Stop = true;
    }
  }
  // Primordial thread creates real-time startup thread
  public static void main(String args[]) {
    myRealtimeStartup startThr = new myRealtimeStartup();
    startThr.setPriority(PriorityScheduler.getMinPriority(null)+ 8);
    startThr.start();
  }
  public void run() { // Created threads execute this method
    System.out.println("Created thread");
    int count = 0;
    for (;Stop != true;) {  // continue until asked to stop
     count++;
     // Thread.yield();  // yield to other thread
    }
    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

程序的輸出顯示所有的線程運行並結束,但是這兩個線程只有一個執行 for 循環的一個迭代。這個輸出可通過考慮 RT 線程的優先級來解釋。主 RT 線程一直運行,直至調用 Thread.sleep() 方法來阻塞線程。主 RT 線程創建了兩個 RT 線程,但是只有第二個 RT 線程(具有第 6 RT 優先級)才能夠在主 RT 線程休眠時運行。這個線程一直運行,直至主 RT 線程從休眠中蘇醒並指示線程結束。主 RT 線程一旦結束,就允許執行具有第 6 優先級的線程並結束。程序按這種方式執行並打印具有非零值循環計數。此線程結束後,就允許運行具有第 4 RT 優先級的線程,但它只是繞過 for 循環,因為系統指示結束該線程。該線程將打印零循環計數值然後結束。

RT 應用程序的線程化考慮

移植應用程序以使用 RT 線程或編寫新應用程序以利用 RT 線程化時需要考慮 RT 線程化的一些特性,本節將討論這些特性。

RT 線程的新擴展

RTSJ 指定了一些工具,用於創建在某個特定或相關時間啟動的 RT 線程。您可以創建一個線程,用於在指定的時間間隔或時期內運行某種邏輯。您可以定義一個線程,用於未在指定時期內完成此邏輯時執行(激發)一個 AsynchronousEventHandler(AEH)。您還可以定義線程所能夠使用的內存類型和數量的限制,如果超過該限制,則拋出 OutOfMemoryError。這些工具只對 RT 線程可用,而對普通 Java 線程不可用。您可以在 RTSJ 中找到關於這些工具的更多信息。

Thread.interrupt() 和 pending 異常

RT 線程擴展了 Thread.interrupt() 行為。此 API 會像 JLS 中描述的那樣中斷被阻塞的進程。如果用戶在方法聲明中加入 Throws AsynchronouslyInterruptedException 子句,顯式地將其標記為可中斷,也會引起這個異常。該異常也會困擾 線程,用戶必須顯式地清除異常;否則它會一直困擾(術語為 pending)線程。如果用戶不清除異常,則線程會伴隨著該異常而結束。如果線程以 “常規” 形式結束,但是不是在按自身形式進行 RT 線程入池的應用程序中,這種錯誤危害不大,就是說,線程返回池中時仍然隨附 InterruptedException。在這種情況下,執行線程入池的代碼應顯式地清除異常;否則,當重新分配具有隨附異常的入池線程時,可能欺騙性地拋出異常。

原始線程和應用程序調度邏輯

原始線程通常都是普通 Java 線程 —— 而不是 RT 線程。第一個 RT 線程總是由普通 Java 線程創建。如果沒有足夠的可用處理器來同時運行 RT 線程和普通 Java 線程,則這個 RT 線程會立即搶占普通 Java 線程。搶占可以防止普通 Java 線程繼續創建 RT 線程或其他邏輯,以便將應用程序置於適當的初始化狀態。

您可以通過從一個高優先級 RT 線程執行應用程序初始化來避免這個問題。執行自身形式的線程入池和線程調度的應用程序或庫可能需要這種技術。即,線程調度邏輯應該以高優先級運行,或在高優先級的線程中運行。為執行線程入池邏輯選擇適當的優先級有助於防止線程入隊和出隊中遇到的問題。

失控線程

普通 Java 線程按時間量執行,而動態優先級根據 CPU 的使用調整調度程序的執行,允許所有的普通 Java 線程最後執行。反過來,RT 線程不受時間量的限制,並且線程調度程序不根據 CPU 的使用進行任何形式的動態優先級調整。普通 Java 線程和 RT 線程之間的調度策略差異使失控 RT 線程的出現成為可能。失控 RT 線程可以控制系統並阻止所有其他應用程序的運行,阻止用戶登錄系統等等。

在開發和測試期間,有一種技術可以幫助減輕失控線程的影響,即限制進程能夠使用的 CPU 數量。在 Linux 上,限制 CPU 的使用使進程在耗盡 CPU 限制時終止失控線程。另外,監控系統狀態或提供系統登錄的程序應該以高 RT 優先級運行,以便程序可以搶占問題線程。

從 Java 優先級到操作系統優先級的映射

在 Linux 上,POSIX SCHED_FIFO 策略提供了從 1 到 99 的整數范圍內的 99 個 RT 優先級。在這個系統范圍內,從 11 到 89 的優先級由 WebSphere VM 使用,此范圍的一個子集用來實現 28 個 RTSJ 優先級。28 個 RT Java 優先級映射到此范圍的 POSIX 系統優先級,IBM WebSphere Real Time 文檔中對這一點作出了描述。但是應用程序代碼不應該依賴這個映射,而只應該依賴於 Java 級別的 28 個 RT 優先級的相關順序。這樣 JVM 可以在未來的 WebSphere Real Time 版本中重新映射這個范圍並提供改進。

如果某些端口監督程序或 RT 進程需要的優先級高於或低於 WebSphere Real Time 中使用的優先級,則應用程序可以使用 SCHED_FIFO 優先級 1 或優先級 90 來實現這些程序或進程。

JNI AttachThread()

Java Native Interface (JNI) 允許使用 JNI AttachThread() API 將使用 C 代碼創建的線程加入到 JVM 中,但 RTSJ 並不對 JNI 接口進行更改或配置以便加入 RT 線程。因此,應用程序應避免用 C 代碼創建准備加入到 JVM 中的 POSIX RT 線程。反過來,應該在 Java 語言中創建此類 RT 線程。

派生進程和 RT 優先級

一個線程可以派生另一個進程。在 Linux 上,派生進程的原始線程繼承派生它的父線程的優先級。如果派生進程是一個 JVM,則 JVM 的原始線程創建時具有 RT 優先級。這將與普通 Java 線程的順序沖突,比如原始線程的調度優先級比 RT 線程低。為了防止這種情形,JVM 強制原始線程擁有非 RT 優先級 —— 即擁有 SCHED_OTHER 策略。

Thread.yield()

Thread.yield() 只讓步給具有相同優先級的線程,決不會讓步給高於或低於自身優先級的線程。只讓步給具有相同優先級的線程意味著在使用多個 RT 優先級的 RT 應用程序中使用 Thread.yield() 可能會出現問題。應該避免使用 Thread.yield(),除非完全有必要。

NoHeapRealtimeThreads

javax.realtime.NoHeapRealtimeThread (NHRT) 是 RTSJ 中的另一種新的線程類型,它是 javax.realtime.RealtimeThread 的一個子類。NHRT 具有與我們所描述的 RT 線程相同的調度特征,只是 NHRT 不會被 GC 搶占並且 NHRT 無法讀或寫 Java 堆。NHRT 是 RTSJ 的一個重要方面,本系列的後續文章中將對它進行討論。

AsynchronousEventHandlers

AsynchronousEventHandler (AEH) 是 RTSJ 附帶的新增程序,可將它視為發生事件時執行的一種 RT 線程。例如,可以設置 AEH 在某個特定或關聯時間激發。AEH 還具有與 RT 線程相同的調度特征並具有堆和非堆兩種風格。

同步概述

許多 Java 應用程序直接使用 Java 線程化特性,或正在開發中的應用程序使用涉及多個線程的庫。多線程編程中的一個主要考慮是確保程序在執行多線程的系統中正確地 —— 線程安全地 —— 運行。要保證程序線程安全地運行,需要序列化訪問由多個使用同步原語(如鎖或原子機器操作)的線程共享的數據。RT 應用程序的編程人員通常面臨使程序按某種時間約束執行的挑戰。為了應對這個挑戰,他們可能需要了解當前使用組件的實現細節、含意和性能屬性。

本文的剩余部分將討論 Java 語言提供的核心同步原語的各個方面,這些原語在 RTSJ 中如何更改,以及 RT 編程人員使用這些原語時需要注意的一些暗示。

Java 語言同步概述

Java 語言提供了三種核心同步原語:

同步的方法和代碼塊允許線程在入口處鎖定對象並在出口處解鎖(針對方法或代碼塊)。

Object.wait() 釋放對象鎖,線程等待。

Object.notify() 為 wait() 對象的線程解鎖。notifyAll() 為所有等待的線程解鎖。

執行 wait() 和 notify() 的線程當前必須已經鎖定對象。

當線程試圖鎖定的對象已被其他線程鎖定時將發生鎖爭用。當發生這種情況時,沒有獲得鎖的線程被置於對象的鎖爭用者的一個邏輯隊列中。類似地,幾個線程可能對同一個對象執行 Object.wait(),因此該對象擁有一個等待者的邏輯隊列。JLS 沒有指定如何管理這些隊列,但是 RTSJ 規定了這個行為。

基於優先級的同步隊列

RTSJ 的原理是所有的線程隊列都是 FIFO 並且是基於優先級的。基於優先級的 FIFO 行為 —— 在前面的同步示例中,將接著執行具有最高優先級的線程 —— 也適用於鎖爭用者和鎖等待者的隊列。從邏輯觀點來看,鎖爭用者的 FIFO 基於優先級的隊列與等待執行的線程執行隊列相似。同樣有相似的鎖等待者隊列。

釋放鎖以後,系統從爭用者的最高優先級隊列的前端選擇線程,以便試圖鎖定對象。類似地,完成 notify() 以後,等待者的最高優先級隊列前端的線程從等待中解除阻塞。鎖釋放或鎖 notify() 操作與調度分派操作類似,因為都是對最高優先級隊列頭部的線程起作用。

為了支持基於優先級的同步,需要對 RT Linux 作一些修改。還需要對 WebSphere Real Time 中的 VM 作出更改,以便在執行 notify() 操作時委托 Linux 選擇對哪一個線程解除阻塞。

優先級反轉和優先級繼承

優先級反轉 指的是阻塞高優先級線程的鎖由低優先級線程持有。中等優先級線程可能搶占低優先級線程,同時持有鎖並優先於低優先級線程運行。優先級反轉將延遲低優先級線程和高優先級線程的執行。優先級反轉導致的延遲可能導致無法滿足關鍵的時限。圖 1 的第一條時間線顯示這種情況。

優先級繼承 是一種用於避免優先級反轉的技術。優先級繼承由 RTSJ 規定。優先級繼承背後的思想是鎖爭用,鎖持有者的優先級被提高到希望獲取鎖的線程的優先級。當鎖持有者釋放鎖時,它的優先級則被 “降” 回基本優先級。在剛剛描述的場景中,發生鎖爭用時低優先級的線程以高優先級運行,直至線程釋放鎖。鎖釋放後,高優先級線程鎖定對象並繼續執行。中等優先級線程禁止延遲高優先級線程。圖 1 中的第二條時間線顯示了發生優先級繼承時第一條時間線的鎖定行為的變化情況。

圖 1. 優先級反轉和優先級繼承

可能存在下面一種情況:高優先級線程試圖獲取低優先級線程持有的鎖,而低優先級線程自身又被另一個線程持有的另一個鎖阻塞。在這種情況下,低優先級線程和另一個線程都會被提高優先級。就是說,優先級繼承需要對一組線程進行優先級提高和降低。

優先級繼承實現

優先級繼承是通過 Linux 內核功能來提供的,通過 POSIX 鎖定服務可將後者導出到用戶空間。完全位於用戶空間中的解決方案並不令人滿意,因為:

Linux 內核可能被搶占並且常常出現優先級反轉。對於某些系統鎖也需要使用優先級繼承。

嘗試用戶空間中的解決方案導致難於解決的競態條件。

優先級提高總是需要使用內核調用。

POSIX 鎖的類型為 pthread_mutex。用於創建 pthread_mutex 的 POSIX API 使用互斥鎖來實現優先級繼承協議。有一些 POSIX 服務可用於鎖定 pthread_mutex 和為 pthread_mutex 解鎖。在這些情況下優先級繼承支持生效。Linux 在沒有鎖爭用的情況下執行用戶空間中的所有鎖定。當發生鎖爭用時,在內核空間中進行優先級提高和同步隊列管理。

WebSphere VM 使用 POSIX 鎖定 API 來實現我們先前所描述的用於支持優先級繼承的核心 Java 語言同步原語。用戶級別的 C 代碼也可以使用這些 POSIX 服務。對於 Java 級別的鎖定操作,分配了一個惟一的 pthread_mutex 並使用原子機器操作將其綁定到 Java 對象。對於 Java 級別的解鎖操作,使用原子操作解除 pthread_mutex 與對象之間的綁定,前提是不存在鎖爭用。存在鎖爭用時,POSIX 鎖定和解鎖操作將觸發 Linux 內核優先級繼承支持。

為了幫助實現互斥鎖分配和鎖定時間的最小化,JVM 管理一個全局鎖緩存和一個單線程鎖緩存,其中每個緩存包含了未分配的 pthread_mutex。線程專用緩存中的互斥鎖從全局鎖緩存中獲得。互斥鎖在放入線程鎖定緩存之前被線程預先鎖定。非爭用的解鎖操作將一個鎖定的互斥鎖返回給線程鎖定緩存。此處假定以非爭用的鎖定為標准,而 POSIX 級別的鎖定則通過重用預先鎖定的互斥鎖來得到減少和攤銷。

JVM 自身擁有內部鎖,用於序列化對關鍵 JVM 資源(如線程列表和全局鎖緩存)的訪問。這些鎖基於優先級繼承並且其持有時間較短。

RT 應用程序的同步考慮

本節將介紹 RT 同步的一些特性,這些特性可以幫助移植應用程序的開發人員使用 RT 線程或編寫新的應用程序以使用 RT 線程化。

普通 Java 線程和 RT 線程之間的鎖爭用

RT 線程可能被普通 Java 線程持有的鎖阻塞。發生這種情況時,優先級繼承接管線程,因此普通 Java 線程的優先級被提高到 RT 線程的優先級,並且只要它持有鎖就一直保持該優先級。普通 Java 線程繼承了 RT 線程的所有調度特征:

普通 Java 線程按 SCHED_FIFO 策略運行,因此線程不劃分時間片。

在提高了優先級的 RT 運行隊列中進行調度和讓步。

此行為在普通 Java 線程釋放鎖時返回到 SCHED_OTHER。如果 清單 1 中創建的兩個線程在持有 RT 線程所需要的鎖的時候都不運行,則該程序將不會結束並且出現我們在 使用 RT 線程的問題代碼示例 部分中描述的問題。因為可能出現這種情形,所以對於所有在實時 JVM 中執行的線程來說,執行 spin 循環和讓步並不明智。

NHRT 和 RT 線程之間的鎖爭用

NHRT 可能在 RT 線程(或相應的普通 Java 線程)持有的鎖處阻塞。雖然 RT 線程持有鎖,但是 GC 可能搶占 RT 並間接地搶占 NHRT。NHRT 需要一直等到 RT 不再被 GC 搶占並釋放鎖後才能執行。如果 NHRT 執行的功能具有嚴格的時間要求,則 GC 搶占 NHRT 將是一個嚴重的問題。

WebSphere Real Time 中具有確定性的垃圾收集器將暫停時間保持在一毫秒以下,使 NHRT 搶占更具有確定性。如果不能容忍此類暫停,則可以通過避免 NHRT 和 RT 線程之間的鎖共享來繞過該問題。如果強制使用鎖定,則可以考慮使用特定於 RT 或 NHRT 的資源和鎖。例如,實現線程入池的應用程序可以考慮對 NHRT 和 RT 線程使用分開的池和池鎖。

同樣,javax.realtime 包提供了以下的類:

WaitFreeReadQueue 類主要用於將對象從 RT 線程傳遞到 NHRT。

WaitFreeWriteQueue 類主要用於將對象從 NHRT 傳遞到 RT 線程。

RT 線程在執行無等待操作時可能被 GC 阻塞,上述類保證了 RT 線程在執行無等待操作時不會持有 NHRT 所需的鎖。

javax.realtime 包中的同步

某些 javax.realtime 方法故意沒有實現同步,因為即使鎖是無爭用的,同步也會造成系統開銷。如果需要同步,則調用方負責封裝同步方法或塊中所需的 javax.realtime 方法。編程人員在使用 java.realtime 包的方法時必須考慮添加此類同步。

核心 JLS 包中的同步

相反,如 java.util.Vector 之類的核心 JLS 服務已經實現同步。同樣,某些核心 JLS 服務可以執行一些內部鎖定來序列化某些共享資源。由於這種同步,在使用核心 JLS 服務時,必須謹慎執行以避免 GC 搶占 NHRT 的問題(參見 NHRT 和 RT 線程之間的鎖爭用)。

無爭用鎖定的性能

非 RT 應用程序的標准檢查和檢測已表明鎖定主要是無爭用的。無爭用鎖定同樣被認為是 RT 應用程序中的主要類型,特別是現有組件或庫需要重用的時候。如果已知鎖定是無爭用的但是難以避免或刪除同步指令,則對這些鎖定花費一點小的確定的開銷不失為明智的做法。

如前所述,無爭用鎖定操作涉及了一些設置和一個原子機器指令。解鎖操作涉及一個原子機器指令。鎖定操作的設置涉及分配一個預先鎖定的互斥鎖。該分配被認為是無爭用鎖定操作中最大的可變開銷。RealtimeSystem.setMaximumConcurrentLocks() 可以幫助控制這種可變開銷。

RealtimeSystem.setMaximumConcurrentLocks(int numLocks) 使 WebSphere Real Time 中的 VM 將 numLocks 互斥鎖預先分配給全局鎖緩存。全局鎖緩存將其內容提供給單線程鎖緩存。通過使用這個 RealTimeSystem API,可以降低具有嚴格時間要求的代碼區域中發生鎖定初始化的機率。RealTimeSystem.getMaximumConcurrentLocks() 可以用來幫助決定 setMaximumConcurentLocks() 調用中應該使用的數量,但是要注意 getMaximumConcurrentLocks() 提供關於調用的鎖使用,而不是最高使用標記(high-water mark)。未來的 RTSJ 版本可能提供 API 以便提供最高使用標記。不要為 numLocks 的值提供很大的值,因為調用 setMaximimConcurrentLocks() 可能耗費過量的時間和內存來創建那些鎖。還要注意:這個 API 是定義為 JVM 專用的,因此其他 JVM 可能忽略該調用或提供不同的行為。

爭用鎖定的性能

一個線程可以同時持有多個鎖,並且可能已經按某種順序獲得了這些鎖。所有此類鎖定模式形成了一個鎖層次結構。優先級繼承可以指提高和降低一組線程的優先級。組中線程的數量應該不大於系統中最深的鎖層次結構的深度。通過保持較淺的鎖層次結構,可以鎖定最少量的對象,您能夠影響最大量的需要調整優先級的線程。

同步操作中的時間

Object.wait(long timeout, int nanos) 為相關的等待操作提供納秒粒度。HighResolutionTime.waitForObject() API 與 Object.wait() 類似並提供可使用納秒粒度指定的相對時間和絕對時間。在 WebSphere Real Time 中,這兩個 API 都使用底層 POSIX 鎖定等待服務來實現。這些底層服務最多提供微秒粒度。如有需要,出於可移植性目的,javax.realtime 包的 Clock 類的 getResolution() 方法應用於檢索執行平台的分辨率。

結束語

RTSJ 通過 javax.realtime 包中的新 RT 類和 API 擴展並加強了 Java 編程人員的線程化和同步功能。在 WebSphere Real Time 中,這些功能通過 Linux 內核的 RT 版本來實現,對 POSIX 線程化進行修改並對 JVM 自身進行修改。您現在對 RTSJ 線程化和同步有了更深入的了解,這些知識可以幫助您在編寫和部署 RT 應用程序時避免問題的發生。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved