移植应用程序以使用 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 资源(如线程列表和全局锁缓存)的访问。这些锁基于优先级继承并且其持有时间较短。