谈到“实时计算(real-time computing)”,人们普遍存在一种误解,即认为“实时系统”一定就是运行得很快的系统,而且几乎只用于机械控制系统。在大多数情况下,实时系统的确需要很快的响应速度,但是仅有“速度”是不足以定义实时系统的。实时环境的真正核心在于,系统必须保证在预定义的时间内执行完指定的任务,这样它的行为才是完全确定的。
对企业级应用程序来说,部署在实时系统上有什么优势(或者劣势)呢?大多数情况下,没有明显的优势。只要满足了非功能性需求(负载能力、平均响应时间、峰值响应时间等),应用程序就可以部署,用户也会觉得满意。有些情况应不应该出现——比如发给 HR 应用的请求等待响应的时间比普通的用户请求还要长——其实它产生的影响并没有一个可以测量的标准。但是,对于大多数金融、企业级应用程序来说,不能在规定的时间内完成某些任务很容易产生其它开销——也许是很大的开销。按照金融市场自己准确的说法,市场是瞬息万变的,计算机交易系统也意味着价格会以每秒钟数次的频率变化。如果系统中的某个部分决定以当前的价格进行一笔交易,但是由于某些原因这笔交易被延迟了,那么价格的一点微小变化也许都会给用户带来巨大的损失。如果这种延迟频繁发生,那说明系统已经是问题重重了。
事实证明,Java 语言和 Java 企业版平台在企业应用程序开发中已经非常流行了。易于开发、性能及可靠性都令 Java 对开发者充满了诱惑。但是,Java 平台并不支持实时应用程序,即使在实时操作系统上运行 Java 程序,也不能让程序获得更多的确定性。对 Java 应用程序来说,确定性行为的最大障碍是由 Java 虚拟机(JVM)托管的内存管理方式。与早期的 C 和 C++ 这些语言不同,Java 使用垃圾回收器来回收那些应用程序不再使用的内存。这样做的原因是为了消除“内存泄露”——程序员忘记显式地释放那些不再使用的内存资源。机器中的内存数量是有限的,如果这个错误出现在某些循环中,那么系统最终会把内存耗尽。(这并不是说,你可以在 Java 中完全不顾内存泄露的问题;如果开发者占有一个指向对象的引用不释放,垃圾回收器仍然不能回收对象占用的内存)。
垃圾回收器使用一个后台线程监控堆空间的变化,“堆”是存储所有 Java 对象的场所。垃圾回收器可以识别出不再被引用的对象,并回收空闲出的内存。这些工作大多需要在堆内部将对象从一处复制到另一处。为了防止可能出现的数据崩溃,回收内存时,所有的应用线程(指那些会引起数据变化的线程,mutator threads)都必须暂停。现在的 JVM 中,由于已经对桌面应用程序进行了高度调优,这些暂停都不易察觉到。甚至对大部分企业级应用程序来说,并行标记扫描(concurrent-mark-sweep)回收器(也叫低暂停回收器,low-pause collector)的应用也已经把暂停降低到了一个大多数应用可以接受的级别上。但不确定行为使 Java 并不适用于那些类似于前面提到的关键级别企业应用程序。
为了解决这个问题,JCP 组织(Java Community Process)创建了一个 Java 规范请求(JSR),专门用于设计和实现 Java 的实时规范(RTSJ)。事实上,在起始于 1998 年的 JCP 中,它是一个非常早期的 JSR。JSR 的专家组为规范的创建指定了几条指导原则。当我们考虑企业级应用程序的适用性时,其中的三条原则值得关注:
- 向后兼容性。任何兼容的 Java 编译器生成的类文件都能继续在 RTSJ 虚拟机上运行。
- 不扩展现有的语法。该规范不应该给 Java 语言增添新的关键字或改变语法。这条原则的部分起因是第一条原则,但为了让 Java 开发者容易地移植到 RTSJ、可以继续使用现有的 NetBeans 等开发工具,这条原则也是很有必要的。
- 可预测的执行行为。这是决策时优先级最高的原则。这条原则有时会影响典型的计算性能有所下降。正是出于这个原因,考虑在通用目的的企业级程序中应用 RTSJ 时应该格外慎重。
RTSJ 详述了扩展了 Java 语义的八个领域:
- 调度(Scheduling)
- 内存管理(Memory management)
- 同步(Synchronization)
- 异步事件处理(Asynchronous event handling)
- 异步的控制转移(Asynchronous transfer of control)
- 异步终止线程(Asynchronous thread termination)
- 访问物理内存(Physical memory access)
- 异常(Exceptions)
RTSJ 中的线程可以是下述三种类型之一:非实时线程、软实时线程和硬实时线程。非实时线程比较容易理解,因为它不会为某些动作设置必须完成的具体期限。JVM 可能在任何方便的时候调度它们,垃圾回收产生的影响也是微不足道的。软实时线程会为动作的完成设置一个最终期限。但它的“最终期限”有一定的回旋余地,因此,即使一个动作比规定的时间稍微晚一些才完成,所有的事情仍然能够正常运行、不会出现任何问题。对硬实时线程来说,它们要求动作必须在最终期限之前完全完成;如果做不到的话,会产生一个不可恢复的(unrecoverable)错误。RTSJ 应用程序可以同时运行这三种类型的线程。选择哪种类型的线程,则由应用程序的设计者和适用于线程所做工作的重要级别来决定。下图显示了这些不同类型的线程之间的关系:
图 1 RTJS 中的线程
典型地,线程要正常工作就必须与应用程序交换数据。由于硬实时线程不能依赖于非实时线程为它实时地发送结果,因此使用一个无等待(wait-free)的数据传送队列可以保证硬实时线程不会被其他线程阻塞。
使用现有的 Thread 类、不做任何改动,就能创建一个非实时线程。创建软实时线程需要使用 RealtimeThread 类,它是 Thread 的子类。这个类的构造函数可以带有可选的优先级参数和释出(release)参数,以此来定义 JVM 如何调度线程。创建硬实时线程则需要使用 RealtimeThread 的子类 NoHeapRealtimeThread。从这个类的名称中,你或许能够获得 JVM 如何实现硬实时线程的一些线索;讨论内存管理的时候,我们还会再详细地讨论这个问题。
把现有的应用程序转化为实时应用程序,最简单的做法就是用 RealtimeThread 类简单替换发起新线程的代码。虽然这样只把应用程序转为了软实时,但是可以看出,转换过程非常容易。
标准的 Java Thread 类包含了“优先级”的概念,比起低优先级的线程,优先级高的线程会被优先选择。在 RTSJ 中,这个概念被进一步扩展了,规范中称,实现必须支持至少 28 个不同的优先级。因为优先级是用整数表示的,所以规范的实现可以提供更多的优先级别。规范还指出,高优先级线程永远优先于低优先级线程,当更高优先级的线程开始运行时,它会抢占当前运行线程的位置。图 2 中的类表现了上述信息:
图 2 线程执行优先级
PriorityParameters 类封装了表示线程优先级的整数。如果多个线程具有相同的优先级,那么可以用一个 importance 属性与线程关联起来,来指定哪个线程可以获得更大的优先权。
实时系统中有一个很重要的观念:实时系统应该有能力提前判断出它们是否满足应用程序的需求。为了做到这一点,系统必须通过单调速率分析(rate monotonic analysis)知道任务的细节。RTSJ 通过释放参数收集这些信息。类的层次结构如图 3 所示:
图 3 释放参数
ReleaseParameters 类包含线程的时间开销(这是平台相关的,如果应用程序迁移到不同的平台上,结果也会有变化)、任务必须完成的最终期限,以及处理超出时间开销或错过最终期限这两种情况的处理器。可以处理的线程的类型既可以是周期性的,也可以非周期的,在前面的情况中,线程会按照一个固定的频率重复执行,后者则是线程可以在任何时候启动。如果硬实时系统中包含非周期的任务,这种情况下是不可能分析正确性的,所以引入了“零星任务(sporadic task)”的概念。零星任务为非周期任务分配一个最小频率,这样可以把它当做周期任务对待,从而能够对系统进行分析。将一个非周期任务转化成零星任务并不会改变程序运行的方式,它只是让系统能够判断自己是否满足实时的需求了。
RTSJ 还必须实现一种机制,以防止所谓的优先级倒置(priority inversion)现象。图 4 说明了这个问题:
图 4 优先级倒置
就像规范要求的那样,一个低优先级线程(优先级 P3)获取了一个对象的锁,会被一个高优先级(优先级 P1)的线程夺取执行权。P1 线程需要同一个对象上的锁,但是在 P3 线程释放它之前,P1 无法得到锁。其他次高优先级(优先级 P2)的线程不断地阻碍 P3 运行和释放锁。实际看到的效果是 P2 优先级的线程要优先于 P1 线程,而这并不是规范里要求的。有两种方法可以避免这种情况。第一种是优先级继承。如果系统检测到 P3 线程正在持有一个 P1 线程需要的锁,则会把 P3 线程的优先级置为 P1,直到它释放了锁。这种方法不需要开发者修改任何代码。第二种方法是使用优先级封顶模拟(priority ceiling emulation)。对于第二种方法,开发者必须清楚一个事实:持有锁的线程需要晋升自己的优先级,而且必须在代码中显式地调用来达到这一目的。这种方法在 RTSJ 中是可选的。
对于内存管理,RTSJ 用到了一个“内存域”的概念。它的类结构如图 5 所示。
图 5 实时系统的内存域
由于垃圾回收会引发不确定的暂停,因此所有硬实时线程都必须使用不会受垃圾回收器影响的内存。有两种方法可以做到:Scoped 内存和 Immortal 内存。
Scoped 内存就是一块内存,它有一个由应用程序开发者定义的生命期。开发者会创建一个指定大小的 Scoped 内存域,当实例化一个对象的时候,会从这个域内分配一块空间给它。Scoped 内存有两种,线性的(linear)和可变的(variable)——这是指它们实例化一个对象所需的时间。LTMemory 类表示这样的内存域,在这里实例化一个对象的时间等于固定的分配时间加上不定的初始化时间。由于初始化时间直接与对象的大小成比例,因此时间是线性的,而不是常量。VTMemory 类表示的内存域则是这样的,其内存分配机制可以使用任何算法,因此时间也是千差万别的。一旦 Scoped 内存中的所有对象都不再被引用了,它就会被释放以备再次使用。使用 Scoped 内存的一种场景是:创建 Scoped 内存域,以供一个 NoHeapRealTimeThread 线程使用。当线程执行到所有已知对象都不再被引用的时候,内存域就可以被释放了。
Immortal 内存,顾名思义,它永远不会被回收。它被所有线程共享,只有那些开发者确认会随 JVM 一直存在的对象才适合在这里初始化。
RTSJ 规范还允许开发者做一些在标准版 Java 中不可能办到的事:直接访问物理内存。不过这项功能更多地用于嵌入式实时应用,而不是企业应用,所以本文就不做过多的讨论了。
正如你看到的,RTSJ 提供了范围非常广的功能,允许企业级应用程序可以混合运行非实时线程、软实时线程和硬实时线程。对金融系统这种“时间就是金钱”的应用程序来说,可以让临界区的代码做为硬实时线程运行,从而避免垃圾回收器产生的不确定性行为。随着 RTSJ 2.0 的发布,已经有一个参考实现可用了,它需要在免费可靠的开源 Solaris 10 操作系统环境下运行,Solaris 10 拥有一个实时的调度器(为了在 JVM 中提供实时的功能,底层的操作系统也必须支持实时的概念)。
企业应用的一个明显趋势是使用具有实时能力的应用服务器。Sun 的工程师已经将 GlassFish 开源应用服务器移植到 RTSJ 规范上了(在 5 个小时之内)。IBM 也正在加紧开发一款实时的 WebSphere 产品。当你为企业应用评估是否使用实时 Java 的时候,请记住:没有免费的午餐。你可能获得了响应时间的保证,但它却会影响到系统的总吞吐量。但是,如果你的 Java 程序的确、的确不能受垃圾回收的影响,必须要在一定的时间内响应,与其对错误的低概率保有侥幸,不如使用 RTSJ。
链接
- http://rtsj.dev.java.net
- http://www.rtsj.org
- http://java.sun.com/javase/technologies/realtime
- http://www-306.ibm.com/software/webservers/realtime/
关于作者
Simon Ritter 专门研究新兴的技术,包括网格计算、RFID、无线传感网络、机器人技术和可穿戴计算。Simon 从 1984 就开始从事 IT 行业了,持有英国 Brunel University 物理学学士学位。他原先从事 UNIT 开发,先后任职于 AT&T UNIT 系统实验室和 Novell,Simon 于 1996 年加入 Sun,并开始了与 Java 技术相关的工作;现在全职从事 Java 开发和咨询。
评论