Java 实时规范(JSR 1)

本文探讨了实时计算的核心概念,强调其关键在于系统的可预测性而非速度。深入解析Java实时规范(RTSJ)如何通过线程优先级、内存管理和任务调度等机制支持实时应用的需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实时计算通常与高速联系在一起,然而这只是实时计算的一部分。实时计算的核心内容是可预测性 —— 保证系统始终在要求的期限内执行。这个时间期限不一定很短(但有时如此),超过这个期限所造成的后果也不一定是灾难性的,只有少数情况会这样。判断某个应用程序是否具有实时性,关键是看它的需求是否包括时态约束(temporal constraints)。

 

  实时计算通常与高速联系在一起,然而这只是实时计算的一部分。实时计算的核心内容是可预测性 —— 保证系统始终在要求的期限内执行。这个时间期限不一定很短(但有时如此),超过这个期限所造成的后果也不一定是灾难性的,只有少数情况会这样。判断某个应用程序是否具有实时性,关键是看它的需求是否包括时态约束(temporal constraints)。

  Java 实时规范(Real-Time Specification for Java,RTSJ)JSR 1 的开发人员对实时计算的定义如下:

  编程环境必须提供必要的抽象以允许开发人员正确地推断应用程序逻辑的当前行为。不一定要非常迅速或轻便,或者专门针对工业控制;它只重视执行应用程序逻辑时对时间的可预测性。

  实时应用程序被划分为一组任务,其中一些任务具有时间期限。目标是对所有任务进行调度,使它们在自己的时间期限内完成(如果有的话)。

  定义:硬实时和软实时

  根据系统需求,实时系统可以分为不同种类。硬实时系统表示系统必须满足所有时间期限而不允许失败。通常,这种系统具有很短的延迟,发生触发事件和开始或完成响应之间的时间通常在几微秒或几毫秒内。

  很多硬实时系统被进一步分为安全关键型系统。这些系统用于保护人类不受伤害和危险。安全关键型系统在进行认证之前必须经过严格的测试并逐行审查代码。注意安全关键型 Java 技术可以通过 JSR 302 学习,在撰写本文时 RTSJ 还没有解决该问题。

  另一方面,根据规范,软实时系统表示如果系统偶尔错过了时间限制,系统仍然可以正常运作。例如,一个 cellular 电话系统就是一种软实时系统:客户期望所有呼叫都能完成,但是偶尔超过了时间期限也是可以接受的,例如音频丢失或延迟建立呼叫。软实时系统通常指定允许错过的时间期限百分比或多久出现一次是可以接受的。

  基于 Java 技术的应用程序的不可预测性

  很多因素都会影响对执行时间的预测,并因此造成标准的 Java 任务错过时间期限。下面是最常见的情况。

  • 操作系统调度。在 Java 技术中,线程由 JVM* 创建,但最终由操作系统调度程序进行调度。JVM 要保证准确的时间延迟,即对某个操作的反应时间的延迟,操作系统必须提供调度延迟保障。因此,操作系统必须提供诸如高分辨率计时器、程序定义的低级中断和健壮的基于优先级的调度程序等功能。
  • 优先级反转。如果应用程序的线程具有不同的优先级,那么一种危险就是优先级反转。如果某个低优先级的线程和某个高优先级线程共享一个资源,并且该资源使用锁保护,那么很可能在高优先级线程需要该锁时,这个锁被低优先级线程持有。在这种情况下,除非低优先级线程完成,否则高优先级线程无法继续进行 —— 这将造成高优先级线程错过时间期限。

  此外,如果在其期间要运行其他不依赖共享资源的中级优先级任务,那么它将优先低和高优先级任务。优先级反转的结果是将高优先级线程的级别"降低"到低优先级线程的级别。

  • 类加载、初始化和编译。Java 语言规范要求延迟初始化类 —— 在应用程序首次使用类时。在第一次使用类时,这种实例化可能执行用户代码,生成抖动(jitter),这是延迟的一种变异。此外,规范允许类被延迟载入。由于加载类需要在磁盘或网络中查找类定义,引用前面未引用的类会造成意想不到的延迟(可能较大)。并且,JVM 可以决定何时将某个类从字节码转换为原生码。通常,只有某个方法执行的频率足够高,可以抵销编译成本时,才会对这个方法执行编译。
  • 垃圾收集。造成 Java 应用程序不可预测的主要原因是垃圾收集(GC)。标准 JVM 使用的 GC 算法都涉及一个停止所有处理(stop-the-world)暂停,发生这种暂停时,所有应用程序线程都将停止,因此垃圾收集可以不受干扰地进行。具有硬响应时间需求的应用程序不能容忍长时间的 GC 暂停,所谓的低暂停垃圾收集器仍然不能保证可预测性,因此仍然需要进行大量调优和测试。
  • 应用程序。造成不可预测性的另一个主要原因是应用程序本身,包括它使用的任何库。大多数应用程序包含多个计算活动,它们公平竞争 CPU 资源。Java 应用程序通常不使用线程优先权,部分原因是因为 JVM 为线程优先权提供了这种无力的保证。在一个具有恰当供给的应用程序中,应该有足够的 CPU 周期可用,但是完成某个任务的时间可能会超出预期 —— 这可能造成其他任务必须等待 CPU 资源。

    大多数开发人员经常要处理这个问题:他们查找最严重的瓶颈,进行改进,然后查找下一个瓶颈并改进,反复执行这些步骤,知道应用程序达到可接受的性能。不过,由于  开发过程本身是不可预测的,它经常会影响交付进度或需要进行大量额外测试。

  • 系统中的其他活动。系统中发生的其他高优先级活动 —— 例如硬件中断、其他实时应用程序等等 —— 会造成应用程序抖动,从而影响可预测性。

  Java 实时规范(RTSJ)

  Java 实时规范(RTSJ)或 JSR 1,指定了 Java 系统在实时环境下应该如何行为,该规范历经来自 Java 和实时领专家的多年开发。

  RTSJ 的目标是无缝扩展任何 Java 系列 —— 不论是 Java Platform, Standard Edition (Java SE)、Java Platform, Micro Edition (Java ME) 还是 Java Platform, Enterprise Edition (Java EE) —— 并且要求任何实现都要通过 JSR1 技术兼容性工具包(TCK)和它所基于的平台的 TCK(Java SE、Java ME 或 Java EE)。

  RTSJ 引入了一些支持实时应用程序的新特性。这些特性包括新的线程类型、新的内存管理模型和其他新引入的框架。让我们看一看 RTSJ 是如何看待任务和时间的。

  任务和期限

  RTSJ 将应用程序的实时部分建模为一组任务,每个任务都有一个时间期限。任务的时间期限指任务必须完成的时间。根据开发人员预测到的任务的频率和时间,实时任务可以分为若干个类型:

  周期性:按照固定时间表运行的任务,例如每 5 毫秒读取一次传感器

  偶然:没有按照固定时间表运行的任务,但是具有一个最大频率

  非周期性:频率和时间无法预测的任务

  RTSJ 在多个方面使用这种任务类型信息,确保关键任务不会错过它们的时间期限。首先,RTSJ 允许您将每个任务关联到一个 deadline miss 处理程序。如果在最后期限之前没有完成任务,将调用这个处理程序。

  错过最后期限的信息可以用于部署中,以便采取纠正措施或向用户或管理程序报告性能或行为信息。经过比较,在非实时应用程序中,在出现副作用(例如请求超时或内存耗尽)以前故障不是很明显,然而此时已错过了发现故障的最佳时机。

  错过时间期限的行为将由 deadline miss 处理程序处理,如下所示:

 

ReleaseParameters.setDeadlineMissHandler( 

AsyncEventHandler handler);

      或者,如果没有处理程序,它将由线程本身执行。

 


 if (waitForNextPeriod() == false) { 

     handle_deadline_miss();

}

      线程优先权

  在一个真正的实时环境中,线程优先权非常重要。系统无法保证所有任务都可以按时完成。不过,实时系统可以保证,如果某些任务错过了执行期限,将首先牺牲低优先级任务。

  RTSJ 定义了至少 28 个优先级级别,并且严格执行。然而,正如本文前面提到过,RTSJ 实现依赖于实时操作系统支持多个优先级,以及使高优先级线程抢占低优先级线程的能力。

  优先级反转问题会破坏优先处理的效率。因此,RTSJ 要求对其调度实现优先级继承(priority inheritance)。优先级继承可以避免优先级反转问题,方法是提升持有锁的线程的优先级,使它具有与等待该锁的优先级最高的线程相同的优先级。

  这可以防止较高优先级的线程处于等待状态,因为较低优先级线程持有所需的锁,但是没有足够的 CPU 周期完成任务,因此也不能释放锁。这个特性也可以防止不依赖共享资源的中等优先级的任务抢占高优先级任务。

  此外,RTSJ 被设计为允许非实时和实时行为同时共存于单个 Java 应用程序内。为某个行为提供的时间保证的程度取决于执行该行为的线程的类型:java.lang.Thread 或 javax.realtime.RealtimeThread 线程类型。

  • 标准 java.lang.Thread (JLT) 线程支持非实时行为。JLT 线程可以使用 Thead 类指定的 10 个优先级级别,但是这些级别不适合实时行为,因为它们不能提供对执行时间的保证。
  • RTSJ 还定义了 javax.realtime.RealtimeThread (RTT) 线程类型。RTT 还可以利用 RTSJ 提供的更健壮的线程优先级,并且根据 run-to-block 进行调度,而不是根据时间片调度。也就是说,如果另一个具有更高优先级的 RTT 变为可执行,那么调度程序将抢占当前 RTT。

  <>内存管理扩展

  标准虚拟机(VM)使用的自动内存管理的一个问题就是某个行为可能需要为另一个行为的内存管理开销 “买单”。考虑一个具有两个线程的应用程序:一个高优先级线程(H)只获得少量内存分配,而另一个低优先级线程(L)获得了大量内存分配。

  如果 H 在运行的时候,堆内的几乎所有可用内存都被 L 占用,当 H 开始收集某个小对象时,垃圾收集器将开始工作,并会运行相当长的时间。现在,H 就开始为 L 消耗的巨大内存付费,即不合理的长时间延迟。

  RTSJ 提供了一个 RTT 的子类,称为 NoHeapRealtimeThread (NHRT)。这个子类的实例不会发生由 GC 引入的抖动。NHRT 类主要针对硬实时行为。

  要最大化可预测性,NHRT 不允许使用垃圾收集堆,也不允许操作堆的引用。否则,线程将因为垃圾收集而暂停,而这将造成任务错过时间期限。实际上,NHRT 可以使用作用域内存(scoped memory)和 不朽内存(immortal memory) 特性,从而更有预测性地分配内存。

  下面的两个 NHRT 示例演示了这一点。

  NHRT -- 示例 1

 

public class PeriodicThread { static NoHeapRealtimeThread nhrt; static { int prio = PriorityScheduler.instance().getMaxPriority(); RelativeTime period = new RelativeTime(20,0); nhrt = new NoHeapRealtimeThread( new PriorityParameters(prio), new PeriodicParameters( period), ImmortalMemory.instance()) { public void run() { . . . . . }; }; } . . . . . }

   NHRT -- 示例 2

 

public class MyNHRT extends NoHeapRealtimeThread { public MyNHRT() { super(new PriorityParameters(5), ImmortalMemory.instance()); setReleaseParameters( new PeriodicParameters( new RelativeTime(0, 0), new RelativeTime(200, 0), new RelativeTime(50, 0), null, null, null)); } }

   内存区

  RTSJ 根据执行分配的任务的性质,提供了多种对象分配方法。对象可以从一个特定的内存区分配,并且不同的内存区具有不同的 GC 特征和分配限制。

  • 标准堆。和标准 VM 一样,实时 VM 维护一个垃圾收集堆,可以用于标准线程(JLT)和 RTT 线程。从标准堆进行分配,会导致线程发生垃圾收集暂停。几种不同的 GC 技术可以平衡吞吐量、可伸缩性、决策和内存大小。要防止发生这种平衡,NHRT 不能使用标准堆,因为这是造成不可预测性的根源。
  • 不朽内存。 不朽内存是一种非垃圾收集内存区。一旦从不朽内存分配某个对象后,那么这个对象使用的内存,从理论上讲,永远不会被收回。不朽内存的主要应用是通过提前静态分配所需的内存并进行管理,可以避免对行为进行动态分配。

    与管理从标准堆分配的内存相比,管理不朽内存更加复杂,因为如果应用程序使不朽对象发生内存泄漏,那么一般都无法收回。在这种情况下,很难将内存返回给不朽内存 区。注意:如果熟悉 C/C++,那么将意识到不朽内存类似于 malloc() 和 free()。

  • 作用域内存。RTSJ 提供了第三种分配机制,称为作用域内存,它只能用于 RTT 和 NHRT 线程。作用域内存主要针对具有已知生命周期的对象,例如在任务处理期间创建的临时对象。和不朽内存一样,作用域内存不会被垃圾收集,但是不同之处是当生命周期结束后,作用域内存区将被完整收回,例如在任务结束后。

    作用域内存区还提供了内存预算。在创建作用域内存区时,将指定它的最大大小,如果您试图超过这个大小,将抛出 OutOfMemoryError 错误。这可以确保某个恶意任务不会消耗掉所有内存,造成其他任务(可能包括高优先级)没有内存可用。

 


                                                                                 为什么使用作用域内存?
  在 Java 程序中,在计算过程中经常会分配临时对象。例如,将两个字符串连接起来将生成一个 StringBuffer 对象,当结果字符串可用时,将丢弃 StringBuffer 对象。
  Java 程序的标准编程风格支持产生一定数量的垃圾,这是几乎所有计算都会产生的副作用,很多库就是这样。由于不能使用硬实时任务的库代码是不可接受的限制,因此需要使用一种机制,使应用程序的成本和临时对象的重新分配变得可预测。
  作用域内存正是这样一种机制。它为任务提供了一种方法,表示 “将在执行这个任务期间要使用这么多临时内存,并在任务结束后释放”。
  由于在任务开始时对内存进行了预分配,因此分配不会失败。因为在任务完成后,作用域内存区的所有内存是空闲的,因此可以快速进行重新分配,并且是可预测的。因此,只要实时任务可以精确预测它们完成工作所需的内存量,作用域内存就可以消除临时内存分配的不可预测性。
  不过,作用域内存的一个缺点就是,由于其中的所有对象将在任务完成后消失,因此无法将作用域内存区的某个对象的引用保存到不朽或堆区域中。如果需要创建具有较长生命周期的对象,则必须要在不同的作用域创建。您可以创建一个区域层次结构,每个子结构要比其父结构具有更短的生命周期。
  在一个程序中,例如任何 JVM 执行的 null 指针和数组界检查,实时 Java 动态地执行分配检查,以保证某个对象永远不会被另一个具有更长生命周期的对象引用。

      对于每一个线程,总是存在一个活动的内存区,称为 当前分配上下文(current allocation context)。某个线程分配的对象都是从这个区域中分配的。当执行具有特定内存区的代码块时,当前分配上下文将发生变化。

  内存区 API 包含一个 enter (Runnable) 方法,允许使用指定任务所在的区域作为当前分配上下文执行该任务。因此,如果您的代码使用第三方库分配代码,仍然可以在作用域内存区使用该代码 —— 并且当当前分配上下文结束后,由该代码分配的所有临时对象都将消失。

  线程之间的高级通信

  RTSJ 的一个优点是它允许实时和非实时行为共存于一个 VM 中。然而,线程之间的通信涉及到内存,因此这将提出一个挑战。线程间通信的一种显而易见的机制是队列,其中一个线程将数据放入到队列中,另一个线程从队列中移除数据。

  假设一个 RTT 以 10 毫秒的时间间隔向队列放入数据,而另一个非 RTT 则使用队列中的数据,那么如果非 RTT 无法获得足够的 CPU 时间消耗队列,会发生什么呢?这个队列将一直增长,并且您有以下三种选择:

  •   队列无限制增长,最终可能会消耗掉所有内存。
  •   必须从队列中删除数据。
  •   RTT 将被阻塞。

  第一种选择并不可行,而最后一个选择是无法接受的。实时行为的可预测性不应该被低优先级活动的行为影响。RTSJ 支持几种类型的非阻塞队列,用于线程间通信。当在实时和非 RTT 之间使用时,对于必须存放元素的内存区施加了一些限制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值