为了在 Java 平台上以通用的方式支持异步交互,人们作出了很多尝试。所有这些尝试都基于一个消息传递通信模型。大部分使用了 actor 模型的一个变体来定义对象。此外,这些框架在可用性、可用库和方法方面各有不同。阶段式事件驱动架构
阶段式事件驱动架构(SEDA)是一种有趣的框架,它将异步编程和自主计算的原理结合在一起。SEDA 是 J2SE 1.4 对 Java NIO API 引入的最大一项补充。该项目本身已经被中断,但是 SEDA 为 Java 应用程序的可伸缩性和适应性设定了新的基准,并且其有关异步 API 的思想对其他项目也产生了影响。
SEDA 试图将异步和同步 API 设计结合起来,产生有趣的结果。这个框架具有比 ad-hoc 并发性 更加良好的可用性,但它还无法达到用户认可的程度。
使用 SEDA,应用程序被划分为若干个阶段。每个阶段表示的组件包含一定数量的线程。请求被分配到一个阶段然后进行处理。阶段可以通过以下几种方式管理自身的容量:
根据负载增加和减少使用线程的数量。这允许服务器动态适应组件的实际使用情况。如果某个组件的使用急剧上升,则会分配更多线程。如果为空闲状态,则减少线程的数量。
根据负载更改行为。例如,可以根据负载生成更加简单的页面。避免对页面使用图像,使用更少的脚本,禁用不必要的功能等等。用户仍然可以使用应用程序,但是生成的请求和通信量将变少。
对试图纳入请求或拒绝接受请求的阶段进行阻塞。
前两种方法非常不错,采用了智能应用程序实现自主计算的思想。然而,第三种方法揭示了为什么该框架至今无法得到广泛应用的原因。除非在设计应用程序时加倍小心,否则这样做会因为增加了死锁风险而引入一个故障点。下面介绍了致使该框架难于使用的其他一些原因:
阶段是一种非常粗粒度的组件。比如网络接口和 HTTP 支持。在将网络层作为整体处理时,很难解决诸如某些客户机带宽有限这样的问题。
无法使用简单的方法返回异步调用的结果。结果只是被分配给阶段,寄希望于阶段能自己找到相关的操作状态。
目前,大多数可用的 Java 库都是同步的。框架并没有尝试以一种一致的方式将同步代码从异步代码中分离开来,从而使编写出的代码很容易意外阻塞整个阶段。
贯彻 SEDA 项目思想的实现中部署最多的可能是 Apache MINA 框架。它用于 OSFlash.org Red5 流服务器的实现、Apache Directory Project 和 Jive Software Openfire XMPP Server。
E 编程语言
严格来讲,E 编程语言是一种动态输入的函数性编程语言,而非一种框架。它强调提供安全的分布式计算,它还为异步编程提供了一些有趣的概念。在异步编程方面,该语言仿效了其前辈 Joule 和 Concurrent Prolog,但是其并发性支持和整体语法更加自然,而且对于拥有主流编程语言(例如 Java 语言、JavaScript 和 C#)背景的编程人员来说也十分友好。
该语言目前通过 Java 和 Common-Lisp 实现。可以通过 Java 应用程序使用。但是,要将其应用于高负荷的服务器端应用程序,仍然存在着一些障碍。大多数问题源于其早期开发,但将来很可能会得到解决。其他一些问题则是由该语言的动态特性引起的,但是这些问题大部分与该语言提供的并发性扩展并无关系。
E 提供了以下核心语言概念来支持异步编程:
vat 表示对象的容器。所有对象都保存在一些 vat 的上下文中,并且不能从其他 vat 同步访问这些对象。
promise 变量用来表示某些异步操作的结果。它的初始状态为未解决状态,表示该操作还未结束。完成操作后,它会获得一个值或者出现 错误。
任何对象都可以接收消息并进行本地调用。本地对象可以通过即时的调用操作进行同步调用,也可以通过最终的发送操作进行异步调用。只能使用最终的发送操作对远程对象进行调用。最终的调用将生成一个 promise。. 操作符用于即时调用,而 <- 操作符用于最终调用。
Promise 也可通过显式方式创建。此时,将提供对解析器对象的引用,并传递给其他 vat。这个对象有两个方法:resolve 和 smash。
when 操作符允许您在 promise 执行 resolve 或 smash 时调用一些代码。when 操作符中的代码被处理为闭包,并且通过访问外围范围中的定义执行。这种方式类似于匿名的内部 Java 类访问一些方法范围内的定义。
这几个概念构成了一个功能强大的可用系统,允许轻松地创建异步组件。即使不是在生产环境中使用该语言,仍然可以使用它原型化复杂并发性问题。它执行一种消息传递规程并提供方便的语法来处理并发性问题。其操作符也十分简单,并且可以在其他编程语言中进行模仿,虽然产生的代码很可能不及原始代码那么优雅和简单。
E 增强了异步编程的可用性。该语言提供的并发支持与其他语言特性毫不相关,并且它可能对现有语言进行了改进。在 Squeak、Python 和 Erlang 开发环境中已经对这些语言特性进行了讨论。与更加特定于域的语言特性(如 C# 中的迭代器)相比,这种语言特性可能更为有用。
AsyncObjects 框架
AsyncObjects 框架项目侧重于使用纯 Java 代码创建可用的异步组件框架。该框架尝试将 SEDA 和 E 编程语言结合在一起。与 E 相同,它提供了基本的并发性机制。同样,它也仿效 SEDA 来提供机制集成同步 Java API。该框架的第一个原型版本发行于 2002 年。自此之后,该框架的开发变得消极起来,但是最近,该项目重新开始活跃。E 已经展示了异步编程的可用性,而这个框架将试图使用纯 Java 代码获得同样的可用性。
和 SEDA 相同,应用程序被分为若干个事件循环。但是,该项目目前还没有实现任何类似 SEDA 的自管理特性。与 SEDA 不同的是,它对 I/O 使用了更加简单的负载管理机制,因为组件更加细粒度化并且 promise 可用来接收操作结果。
框架实现了与 E 编程语言相同的异步组件概念、vat、和 promise。因为不能使用纯 Java 代码引入新的操作符,并不是任意一个对象都是异步组件。实现需要扩展某个基类,并且应该提供由框架实现的异步接口。由框架提供的异步接口实现将向组件的 vat 发送消息,而 vat 稍后将消息分配给组件。
该框架的当前版本(0.3.2)与 Java 5 兼容并且支持泛型。如果当前平台支持的话,也将使用 Java NIO。但是,框架能够返回到普通套接字。
该框架最大的一个问题是类库非常匮乏,因为很难实现与同步 Java API 的集成。目前,只实现了网络 I/O 库。但是,最近作出的一些改进 — 例如 Axis2 中的异步 Web 服务和前面描述的 Tomcat 6 的 Comet Servlet 可以简化这种集成。
Waterken 的 ref_send
Waterken 的 ref_send 框架是使用 Java 编程语言实现 E 思想的又一尝试。它主要通过 Java 语言的一个子集(称为 Joe-E)实现。
该库支持最终的操作调用。然而,与 AsyncObjects 中的支持相比,这种支持的自主性较低。该框架的当前版本还存在线程安全问题。
它只发布了一些核心类和一些非常小的示例,并且没有提供重要的应用程序和类库。因此,关于如何在更大的范围内实现框架思想仍不明确。框架构建者宣称目前正在实现一个完整的 Web 服务器并且即将发布。等到发布之后在重新审视这个框架可能会更加有趣。
Frugal Mobile Objects
Frugal Mobile Objects 是另一种基于 actor 模型的框架。它以诸如 Java ME CLDC 1.1 这样的资源受限环境为目标,使用有趣的设计模式减少资源使用量,同时保持接口具有适当的简单性。
该框架表明应用程序在性能和可伸缩性方面会从异步设计中获益 — 甚至在一个资源受限的环境中。
该框架提供的 API 看似非常繁琐,但是框架的受限目标环境充分证明了这些 API 的有效性。
Scala actor
Scala 是另一种面向 Java 平台的编程语言。它提供了一个 Java 特性超集,但是却使用了稍有不同的语法。与普通的 Java 编程语言相比,它提供了一些可用性增强。
其中一个有趣特性就是基于 actor 的并发性支持,这一点模拟了 Erlang 编程语言。它的设计似乎还没有最终确定,但是这种特性基本可用并且得到该语言的语法支持。然而,与 E 的并发性支持相比,Scala 的跟 Erlang 类似的并发性支持的可用性和自主性较低。
Scala 模型还存在一些安全性问题,因为每条消息都传递对调用方的引用。这使得被调用的组件可以调用调用方组件的所有操作,而不仅仅是返回调用值。就这方面来说,E 的 promise 模型更具粒度化。这种机制用来与阻塞进行通信,目前还没有完全开发完毕。
Scala 的优点在于它可以编译为 JVM 字节码。理论上讲,它可以用于 Java SE 和 Java EE 应用程序,并且不会带来性能损失。然而,对于商业开发的适用性则另当别论,因为 Scala 的 IDE 支持有限,并且,与 Java 语言不同的是,它尚不具备供应商支持。因此,只能用于生命周期较短的项目(如原型),但是,如果对生命周期较长的项目使用该语言,则会添加很多风险。
特定于 Servlet 或特定于 I/O 的 API
由于我们讨论的这些问题只要针对 servlet、Web 服务和一般的 I/O 级别,因此使用了一些项目来专门解决这些问题。这些解决方案的最大缺陷就是它们只针对有限类别的应用程序解决问题。如果不能对本地和远程资源进行异步调用,即使能够实现异步 servlet 也毫无用处。它还应该能够编写一个异步模型和业务逻辑代码。另一个常见问题是解决方案的可用性,通常要低于普通的解决方案。
然而,作为为实现异步组件而作出的努力,这些尝试都值得关注。
JSR 203(NIO.2)
JSR 203 是 NIO API 的改进版。在撰写本文时,它仍然处于初期的草案阶段,在开发过程中可能发生了很多重大修改。其目标是将 API 纳入到 Java 7 中。
JSR 203 引入了异步通道(asynchronous channel)概念。目的是解决众多编程问题,但是似乎 API 仍然非常低级。它最终引入了以前版本所不具备的异步 File I/O API,并且 IoFuture 和 CompletionHandler 概念使它可以更轻松地使用其他框架中的类。一般来讲,新的异步 NIO API 要比上一代 API 中基于选择器的 API 更加易用。甚至可以将它直接用于简单的任务,而不需要编写自定义包装器。
然而,这种 JSR 的一大缺点就是,它高度特定于文件和套接字 I/O。它没有提供构建块来创建更高级的异步组件。可能提供了高级的类,但是必须提供自己的方法来执行相同的任务。这看似是一个不错的技术理念,因为在 Java 语言中仍然没有出现标准的异步组件开发方法。
Glassfish Grizzly NIO
Glassfish Grizzly NIO 支持类似于 SEDA 框架,并且继承了大部分 SEDA 问题。然而,它提供了对 I/O 任务的更加具体化的支持。所提供的 API 要比普通 NIO API 更加高级,但是使用起来仍然很枯燥。
Jetty 6 continuation
Jetty continuation 是一种与传统方法截然不同的方法。甚至可以将之称为一种快速补丁(quick hack)。servlet 可能会请求一个 continuation 对象并调用具有指定超时的 suspend() 方法。该操作将抛出一个异常。然后再对 continuation 调用一个恢复操作,或者 continuation 超过指定时间后自动重新开始执行。
因此 Jetty 尝试实现一个具有异步语义的同步查找 API。然而,这种行为将打断客户机的预测,因为 servlet 将从头执行方法,而不是从调用 suspend() 的位置执行。
Apache Tomcat 6 Comet API
Tomcat Comet API 专门为支持 Comet 交互模式而设计。servlet 引擎通知 servlet 关于其状态转换以及数据是否可读的信息。与 Jetty 使用的方法相比,这种方法更加健全和简单。它使用传统的同步 API 对流执行写入和读取操作。通过使用这种方式实现,如果谨慎使用,则不会出现 API 阻塞的情况。
JAX WS 2.0 和 Apache Axis2 Asynchronous Web Service Client API
JAX WS 2.0 和 Axis2 为 Web 服务的非阻塞调用提供了 API 支持。当 Web 服务操作完成后,Web 服务引擎将通知提供的侦听器。这为 Web 服务的使用提供了新的机会 — 即使来自 Web 客户机。如果一个 servlet 中发生若干独立的调用,它们将并行执行,因此客户机中的总延迟将更低。
结束语
现在,我们已经认识到了异步 Java 组件的必要性,并且,异步应用程序目前正在积极开发之中。两种大型的开源 servlet 引擎(Tomcat 和 Jetty)都至少针对最令开发人员头痛的 servlet 提供了一些支持。尽管 Java 库已开始提供异步接口,这些缺口还缺乏通用的结构,并且,由于线程管理和其他问题,彼此之间很难兼容。因此需要容器能够托管由不同来源提供的各种异步组件。
目前,用户面对着各种各样的选择,每种方法在不同情形下都各有优缺点。例如,Apache MINA 库为一些流行的网络协议提供了现成的支持,因此,在需要使用这些协议的情况下它将是一个不错的选择。Apache Tomcat 6 可以很好地支持 Comet 交互模式,如果要在这种模式中进行异步交互,那么则可以选择使用 Apache Tomcat 6。如果是从头构建应用程序,并且现有库明显不能提供足够支持,那么可以使用 AsyncObjects 框架,因为它提供了各种各样的可用接口。这种框架还可以用于围绕现有异步组件库创建包装器。
现在,是时候为 Java 语言创建一个通用的异步编程框架了。然后,还需要花费很多精力将现有异步组件集成到这个框架中,并为现有同步接口创建一个异步版本。每实现一个步骤,企业 Java 应用程序的可伸缩性都会得到改善,并且我们将能够应对比这更艰难的挑战。持续发展的 Internet 以及不断增生的各种网络服务必定将为我们带来更多这样的挑战。