实时 Java,第 3 部分: 线程化和同步

Java 实时规范中的线程化和同步考虑

本文是关于实时 Java™系列文章(共 5 部分)的第三篇,考察了 Java 实时规范(RTSJ)的实现必须支持的线程化和同步问题。您还将了解开发和部署实时应用程序时必须牢记的一些有关这两方面的基本考虑。

Patrick Gallop (gallop@ca.ibm.com), 高级软件开发人员, IBM Toronto Lab

Patrick Gallop 于 1984 年毕业于 Waterloo 大学并获得数学硕士学位。之后不久就加入了 IBM 多伦多实验室并从事各种编译器和编译器工具项目,包括用于不同操作系统和架构的 C 和静态 Java 编译器。Patrick 致力于多个版本的 IBM Java 项目开发,最近成为 IBM Real-time Java 项目的高级开发人员。



Mark Stoodley, 咨询软件开发人员, IBM Toronto Lab

Mark StoodleyMark Stoodley 在 2001 年从多伦多大学获得计算机工程的博士学位,并于 2002 年加入 IBM 多伦多实验室,研究 Java JIT 编译技术。从 2005 年起,通过采用现有的 JIT 编译器并在实时环境中进行操作,他开始为 IBM WebSphere Real Time 开发 JIT 技术。他现在是 Java 编译控制团队的团队负责人,从事在本地代码的执行环境中提高本地代码编译效率的工作。工作以外,他喜欢收拾自己的家。



2007 年 5 月 24 日

线程化和同步是 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_RRSCHED_FIFO 主要用于具有更高的确定性和时限需求的应用程序。SCHED_RRSCHED_FIFO 之间的主要区别是 SCHED_RR 分时间片 执行线程,而 SCHED_FIFO 则不是这样。SCHED_OTHERSCHED_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 上运行了很多年。

关于本文的代码示例的一点注意

以下各节中的代码示例使用带有让步的 spin 循环。这种方法只适用于演示的目的;不能在 RT 应用程序中使用这种方法。

使用普通 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() 方法。
    • 该线程:
      1. 在每次循环迭代中增加一个计数。
      2. 通过调用 Thread.yield() 方法自愿放弃它的时间片。
      3. 在主线程发出请求时结束。结束前打印循环计数。
  • 优先级为 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_FIFOSCHED_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 应用程序时避免问题的发生。

参考资料

学习

获得产品和技术

  • WebSphere Real Time:WebSphere Real Time 利用了标准的 Java 技术并且没有损失确定性,使应用程序依赖于精确的响应时间。
  • Real-time Java 技术:访问作者的 IBM alphaWorks 研究站点,查找用于实时 Java 的先进技术。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Linux
ArticleID=224386
ArticleTitle=实时 Java,第 3 部分: 线程化和同步
publish-date=05242007