Java RMI 服务器框架

Java RMI 服务器框架

使用异步进程管理器来包含 RMI 服务器应用程序

Java 平台的设计师所设计的远程方法调用(Remote Method Invocation)运行时是个伟大的作品 ― 但它并不打算作为成熟的应用程序服务器。通过从应用程序处理分离 RMI 连接活动,您将可以省去大量的开发的辛苦和压力。本文中,高级 Java 开发者 Edward Harned 介绍了一个框架,通过它您可以刚好达到目的。您可以利用这里出现的代码或把这里的代码修改一下来满足您的应用程序的特定需要。您可以通过单击本文的上部或下部的 讨论来在 讨论论坛与作者和其他读者分享您对本文的看法。

      内容

  1. 为什么使用框架?
  2. 框架的基础
  3. 一个逻辑的预排
  4. 并行处理多部分请求
  5. 看它的工作情况
  6. 框架的可能的增强
  7. 结论
  8. 参考资料
  9. 评论

Edward Harned (ed@coopsoft.com), 高级开发者

2001 年 10 月 15 日

许多 Java 开发者都误认为他们可以现成的将远程方法调用(Remote Method Invocation(RMI))服务器用作成熟的应用程序服务器。这是错误的假定,并随着开发的进展,它将导自极大的痛苦。一个比较好的方法是建立一个围绕 RMI 的框架,由该框架为这样的成熟的应用程序服务器提供结构。

高容量的、可恢复的、安全的、可管理的软件系统的基本组件(最初与事务处理服务器一起引入)是异步(或后端)处理。异步处理的基本流有如下步骤:

  • 通过任何可能的方式来把请求放到设施中。
  • 把请求分成组件部分,并把那些组件放入队列。
  • 使用线程(任务)来并行处理队列,从而减少响应时间和开销。
  • 为下一个可用的输出处理器而把回应放进队列。

隶属于异步处理的基本构件是队列和线程结构。把队列和线程加到 RMI 服务器(一个异步的实体),然后您将得到一个有任务能力的服务器。最好的部分是那并不是很难做到的。

提到线程,大多数开发者都会畏缩;但处理线程是创建我们所要的这种有任务能力的服务器的核心。有太多的关于线程的书和文章,以至于任何明智的人都会对多线程项目感到害怕。但事实上单功能的线程是容易设计的。处理应用程序的多线程的要点是将线程从应用程序分离,并单独的控制每个线程。本文将为您演示如何去做。

RMI 服务器在计算机中作为独立的进程。由于该进程和客户机进程可以彼此独立的运行,所以交互是异步的。异步的进程需要能使其自恃的所需的某一程度的管理 ― 框架。

摘要

本文帮助您理解为什么异步进程需要管理,并概述了最后设计自己的定制的异步进程管理器的必要步骤。

首先,我们来研究单组件异步进程管理器的队列和线程环境。然后,我们将此单组件环境转换为可以并行处理多个队列的请求代理。

把 GUI 应用程序看作同步应用程序。单击按钮,逻辑就在那里处理事件。 RMI 是完全异步的。应用程序逻辑与接口位于不同的 Java 虚拟机(JVM)。您需要发送此请求并接收来自另一 JVM 的回答。

您应该稍微熟悉一下 RMI,至少是已经读了一些在 参考资料部分提到的 RMI 教程。这是个困难的题目,所以我们一次只做一小步。有时您需要看一下演示来掌握要点;本文中我们有一个演示。而且有时您需要查阅代码来真正理解正在做什么;我们也提供了代码。自然的,您应该熟悉同步处理和异步处理的不同。

为什么使用框架?

为什么 EJB 和 GUI 应用程序会如此成功?因为它们在容器 ― 框架(用于管理持久性、消息传递、线程管理、日志记录、事件队列、用户界面等等,还有很多)内运行。

RMI 服务器没有相似的应用程序容器来管理队列、线程、消息传递、日志记录等等。我们必须自己建立它。

RMI 运行时

RMI 运行时是 Java 设计师的一个伟大创作。对于大多数实现来说,RMI 运行时实例化一个线程来处理来自客户机的每个请求。一旦请求完成了,线程为下一个客户机请求等待短暂的时间。通过这种方法,RMI 连接可以重用线程。如果没有新的请求进来,RMI 运行时就销毁这个线程。

开发者有时会忘记 RMI 运行时不是而且也从不应该是成熟的应用程序服务器。使用基本的无应用程序服务的 RMI 运行时会使开发变难。考虑这两个非常基础的问题:

  • 计时:客户机为获得私有资源中包含的信息发送请求到 RMI 服务器。在请求完成之前,如果一组其他用户也在更新该私有资源,则最初的用户将被阻塞。如果私有资源在请求不能完成时,还是无法提供,那么不仅原始的用户被阻塞,而且 RMI 连接线程会永远的挂起。
  • 有回调的自主请求:客户机传递请求,并且在完成时用联系原始客户机的后台线程处理它。如果我们仅仅为每个请求创建一个新的应用程序线程,创建/销毁的开销和线程的数量将给服务器带来严重的压力,并且 JVM 最后将把资源用完。

实用的解决方案

对这些问题和更多的问题的实用的解决方案是从应用程序处理中分离 RMI 连接活动。您可以通过在服务器端创建一个应用程序队列和线程结构来做到这一点。这种是高可靠的、完全任务重要的软件产品的工作方式,而且该结构适用于任何应用程序。

想象适用该模型。对于需要已计时响应的客户机,RMI 连接线程与应用程序线程联系。如果应用程序线程没有在时间限制内响应,那么 RMI 连接线程向客户机返回一个超时消息。对于自主的请求,RMI 连接线程联系应用程序线程,并立即返回客户机一个“已经被调度”的消息。

现在,目标是设计一个队列和应用程序线程环境以便:

  • RMI 连接线程和应用程序线程可以彼此对话。
  • 应用程序环境可以知道客户机超时和恢复。
  • 不会出现线程过载问题。当有太多的应用程序线程在执行以至于 JVM 不能再承担更多的线程时,或是当很多的应用程序线程引起太多的资源竞争以至于环境有效的死锁时,这样的过载就可能会发生。
  • 应用程序线程创建/销毁开销没有使应用程序处理陷入泥沼。
  • 线程环境被监视以准确的确定 停止位(即,自主的请求不能完成的位置)。
  • 整个线程环境可以得体的关闭。

我们将研究您可以运行的异步进程管理器框架。您可以从 参考资料部分下载这些类来执行,也可以下载到源代码。

框架的基础

与任何软件系统一样,框架是由基础的构件组成的。整个结构最初看起来也许很复杂,但它其实不过是建立在彼此之上的组件的共同工作罢了。在接下来的几节中,我们将讨论框架的五个不可缺的元素。

  • 逻辑进程 ― 队列和线程
  • 使用逻辑进程的框架
  • 通用的内存原则
  • 定位应用程序线程
    • 建立基础的、通用的内存锚
    • 把类链接到锚
  • 定位调用 RMI 连接线程

然后,我们用框架来大致处理一个来自客户机的简单的请求。

逻辑进程

操作系统把 Java 虚拟机看作 进程,把线程看作 轻量级进程。我们正研究的异步进程管理器的底层模型被称为 逻辑进程。该模型看起来象什么呢?

逻辑进程的起源

逻辑进程这个术语来自事务处理服务器领域。客户机事务把请求放进后端队列,并让异步任务处理该队列。任务独立于其它的非后端的任务工作 ― 但它们不是物理分离的,只是逻辑上分离的。

考虑一个逻辑进程如在 Amazon.com 完成订单活动。您上线并下定单。由于 Amazon 没有专门针对您的收定单的人,所以您的定单进入定单队列。您继续您的业务。在 Amazon 仓库,某个人从先进先出(FIFO)定单队列拿到您的定单并填充定单。如果您想要一本书,也许定单就是由一个特定仓库的一些可以填写定单的雇员填写的。如果您想要一个盒式录像带,也许定单是由另一个不同的仓库的一些不同的雇员填写的,根据要求仓库也许更大,也许更小。这个定单完成流程是个逻辑进程。

软件中的被管理的进程环境行为相似。在 Amazon,填写定单的是雇员。在软件系统中,填写定单的是应用程序线程。

对后端服务的请求在队列中排队等待。每个 RMI 连接线程把客户机的请求放进队列。应用程序线程从队列中取得请求,并对其做出反应。开发者为不同类型的请求定义队列(象在 Amazon 将放书的仓库和放盒式录像带的仓库分离),并且定义了为每个队列服务的应用程序线程的最大数目。这些是逻辑进程。

图 1. 逻辑进程
逻辑进程

概述异步进程管理器如何使用逻辑进程
客户机向服务器发送请求。RMI 运行时创建或重用一个 RMI 连接线程,该线程执行实现类中的一个方法。恰当的方法会把请求放进队列中的优先等待列表,并且使用对象通知方法唤醒应用程序线程来为请求服务。这里有两种可能的请求:

类对象的等待/通知方法

wait() 使当前的线程等待直到:或者另一个线程为此对象调用了 notify() 方法,或者过了一定长度的时间。notify() 唤醒正服侍该对象的管程的单一的线程。线程通过调用一个等待方法来服侍一个对象的管程。

  • 计时请求:RMI 连接线程使用对象等待方法来挂起执行直到应用程序线程完成了,或者时间限制到期了。到完成时,RMI 连接线程向客户机返回来自应用程序的响应或是一个超时消息。
  • 自主的请求:RMI 连接线程返回客户机一个“已经被调度”的消息。所有的应用程序处理都是异步发生的。

使用逻辑进程的框架

来看一下异步进程管理器的不同部分,它们使事情更真切。关键组件是我们需要使用 RMI 的基本的东西和我们需要实现队列/线程模型的特定于类的应用程序。

基本的 RMI 要素
RMI 要求我们建立如下的东西:

  • 一个接口继承了 java.rmi.Remote
  • 实现接口的声明的具体的实现类
  • 将客户机信息传递给 RMI 服务器的参数类

接口对于该框架,远程接口(如清单 1 所示)需要至少三个方法:

  • 处理定时请求的 syncRequest() 方法
  • 处理自主的请求的 asyncRequest() 方法
  • 客户机可以用来得体的关闭 RMI 服务器的 shutDown()
清单 1. 框架接口
public interface FrameWorkInterface
	extends Remote {
	
	public Object[] syncRequest(FrameWorkParm in)
		throws RemoteException;
		
	public Object[] asyncRequest(FrameWorkParm in) 
		throws RemoteException;
		
	public String shutRequest() 
		throws RemoteException;

具体的实现类
FrameWorkInterface 的实现是 FrameWorkImpl 类。清单 2 是实例域和一部分构造器。因为这是 RMI 连接线程逻辑所在的类(把请求放进请求队列,唤醒一个应用程序线程,将回应返回客户机),所以我们将在本文稍后的地方讨论内部的运作。

清单 2. 框架实现类
public final class FrameWorkImpl 
       	extends UnicastRemoteObject
       	implements FrameWorkInterface {
	// instance field (base of common memory)
	private FrameWorkBase fwb;
    	// constructor
	public FrameWorkImpl (FrameWorkBase reference_to_fwb)
		throws RemoteException {
		
		// set common memory reference
		fwb = reference_to_fwb;

参数类这个类(如清单 3 所示)将客户机信息传递给 RMI 服务器。实例域如下:

  • Object client_data :从客户机到应用程序处理类的可选对象。
  • String func_name :逻辑进程的名称;在本文的后面部分也将其称为 函数 function
  • int wait_time :当使用 syncRequest() 时,RMI 连接线程等待应用程序线程完成的最大的秒数(即,超时间隔)。
  • int priority :请求的优先级。优先级为 1 的请求应该在优先级为 2 的请求前被选择,优先级为 2 的请求应该在优先级为 3 的请求前被选择,诸如此类。
清单 3. FrameWorkParm 类
public final class FrameWorkParm 
	implements java.io.Serializable {
	
 	 // passed data from Client 
  	private Object client_data;
      	// Function name
  	private String func_name;
      	// maximum time to wait for a reply
  	private int wait_time;
     	// priority of the request
  	private int priority;

特定于应用程序的要素
除了为 RMI 而需要的外,应用程序还要求我们建立如下的东西:

  • 创建 RMI 服务器环境的启动类
  • 定义优先级等待列表、应用程序线程和对应用程序处理类的引用的队列类
  • 从等待列表取得请求并调用应用程序处理类的线程类
  • 执行应用程序逻辑的应用程序处理类

启动类该类,如清单 4 所示,启动 RMI 服务器。它包含用于建立持久环境的逻辑。

清单 4. 框架的启动类
public final class FrameWorkServer { 
	// The base for all persistent processing 
	private static FrameWorkBase fwb = null; 
	
	// Start up Server 
	public static void main(java.lang.String[] args){ 
	
		// the base for all processing 
		fwb = new FrameWorkBase(); 
		
		// now, after initializing the other FrameWorkBase fields 
		// including the application queues and threads, 
		// do the Implementation class with a ref to FrameWorkBase
		FrameWorkImpl fwi = new FrameWorkImpl(fwb);

应用程序队列应用程序队列包含三个主要元素:

  • 请求挂起所在的优先等待列表:当没有应用程序线程可以立即对请求作出响应时,请求将在等待列表中排队等待。当线程完成对一个请求的处理时,线程查看列表来获得下一个请求。这通过让每个线程在一个线程执行周期完成多个请求而减少了机器的开销。
  • 应用程序线程的锚点:锚点不过是对实例化的线程的引用。这也许与您更熟悉一些的线程池有所不同。为每个线程使用特定的引用: 
    • 让我们容易的在线程不执行时找到该线程,这样我们就可以杀死它或恢复并重新启动它。
    • 让我们容易的捕获线程来调试。
    • 通过定义每个队列的线程的总数和仅当确实必要的时候才初始化线程的方法来限制线程间的争用并减少线程过载的问题。
    • 通过使队列对每个线程有固定的引用使保留统计非常简单。统计形成了调优的基础。
  • 对应用程序处理类的引用:通常多部分的应用程序类(线程逻辑和应用程序逻辑)被分成了两个独立的类:一个应用程序线程类(只是线程逻辑)和一个应用程序处理类(只是应用程序逻辑)。 

    对于该分离最初也许有点疑惑,因为大多数开发者将线程看作应用程序类的一部分。处理除应用程序逻辑之外的线程逻辑需要将两种思维方式合并起来。该框架设计将线程逻辑从应用程序逻辑分离(我们称为 应用程序处理类),从而应用程序逻辑能够容易的插入线程结构。(参见清单 5。)

    仍然困惑?考虑您用来阅读本文的浏览器。您愿意放首歌或看个录像吗?想想必须把那个逻辑放进基础产品的可怜的浏览器的开发者吧。但如果该增加的逻辑是个插件,那么不仅基础代码小并容易维护,而且您还可以在任何时间安装任何供应商的任何逻辑模块。

清单 5. 队列类
public final class QueueHeader {
    private String  que_name;   // name of this queue
    private WaitLists waitlist; // pointer to wait lists
    private int nbr_waitlists;  // number of wait lists
    private int nbr_wl_entries; // number of entries in a waitlist 
    private int wait_time;      // time to wait when no work
    private int nbr_threads;    // total threads for this queue 
    private QueueDetail[] details; // detail array of thread info
    // public application logic class for use by the Queue Thread
    public DemoCall to_call; // class to call

应用程序线程类。
应用程序线程类包含线程逻辑。每个线程区域(清单 6 中的 QueueDetail 类)包含指向实际线程的指针。当一个 RMI 连接线程唤醒应用程序线程时,应用程序线程从队列的等待列表中取得请求。然后应用程序线程调用应用程序处理类的方法来执行工作并将从那个方法返回的对象传回给 RMI。

清单 6. 线程区域类
public final class QueueDetail {
    private int   status;       // status of this entry
    private String tname;       // name of this thread
    private int  totl_proc;     // total requests processed
    private int  totl_new;      // total times instantiated
    private FrameWorkBase fwb;    // base storage
    private QueueHeader   AH;     // Queue this belongs to
    private QueThread     thread; // queue thread

应用程序处理类。
一个单独的应用程序处理类包含了应用程序逻辑。我们前面所谈到的应用程序线程类调用关于应用程序处理类的方法。对于本示例,我们使用DemoCall 接口(在清单 7 中)。实现该接口的任何类都是可以接受的。

清单 7. DemoCall 接口
public interface DemoCall {
    public Object doWork(Object input, FrameWorkInterface fwi)
    	throws java.lang.Throwable;

看一下该应用程序的 doWork() 方法的两个参数,我们可以知道:

  • 来自客户机的对 FrameWorkParm 实例的引用,以 Object 类型传递(参见 清单 3)。
  • 对服务器自身( FrameWorkImpl )的引用,以 FrameWorkInterface 类型传递。第二个引用是这样的,应用程序可以称服务器 作为一个客户机。这是 递归,是编程中最有用的技术之一,而且有时也是最难实现的之一。

公共内存

该框架后的基本原则是 公共内存的概念。对于任何两个彼此通话的线程,必须使用它们之间是公共的内存。没有一个线程拥有该内存;该内存是可被所有的线程是可更新和可见的。

线程是如何影响公共内存的呢

仅因为在线程间是公共内存,并不意味着线程可以没有任何限制的访问或修改此内存。这在任何语言中都是成立的。 修改在多线程中使用的变量的关键是同步语句或同步方法修饰符。在 Java 语言中,有共享的主内存和线程内存。JVM 从共享主内存给每个线程它所需的一个变量的副本,并且将变量存回共享的主内存以供其它的线程使用。这是 Java 的内存模型。(您可以参阅 Java Language Specification 的第 17 章来获得更多的信息。可以在 参考资料部分找到一个链接)。把对一个对象的访问/改变放进一个同步块或方法可以完成三个功能:

  • 防止其它线程(在同一个对象上同步的)的执行
  • 当线程访问变量时,从共享的主内存读取该变量的当前值到线程存储器
  • 当线程改变了变量时,将线程存储器的变量的新值在块或方法的末尾写到共享的主内存

所以,为保证完整性和确保所有的线程都可以访问变量的最新值, 同步。关于内存访问的基本文字,可以参阅 Brian Goetz 所著的“Can Double-Checked Locking Be Fixed?”。另外,关于多 CPU 线程同步的深入的文章,请参阅 Allen Holub 著的“Warning! Threading in a Multiprocessor World”。您可以在 参考资料部分找到这两篇文章的链接。

RMI 服务器是持久的。这意味着服务器所创建并且还没释放(活动引用)任何对象会为服务器的生命期内保留。当服务器启动时,它获得一个公共内存类( 清单 8 中的FrameWorkBase类 )的新的实例,并且分配一个带有该引用的私有的、静态的域。RMI 实现类( 清单 2 中的FrameWorkImpl 类)也获得对公共内存类的引用。通过这种方式,所有的运行在服务器的线程 ― RMI连接线程和应用程序线程 ― 都可以访问该公共的内存。

图 2 是公共内存和使用它的线程的图示。

图 2. 公共内存
公共内存

对,使用 Java 内存和垃圾回收是有点难理解。我们马上将做演示。然而,在我们开始前,我们需要讨论一下线程是如何彼此发现的。

指针

Java 语言不支持指针计算 ― 但这并不意味着 Java 语言中没有指针。用 Java 中的说法,指针被称为 引用。尽管您不能增加或减少引用,但您还是可以使用引用来定位其它实例化类。沿着这个链走的过程就象第一台计算机一样老。那些熟悉其它有指针的语言(如 C/C++)的人可以在下面的段落中识别出一个通用的技术。

使用公共内存来定位线程

RMI 连接是如何找到应用程序队列并唤醒一个应用程序线程的呢?应用程序线程又是如何唤醒 RMI 连接线程的呢?答案在于环境的结构和指针的使用。


公共内存的基础是 FrameWorkBase 类(参见清单 8)。该类包含对其它类的 静态引用。对于此示例来说,该域是公共的。(这仅是建立公共内存的一种方式。)

清单 8. 公共内存的基础
public final class FrameWorkBase {
    // Function Array
    public static FuncHeader func_tbl = null;
    // Remote Object myself (for recursive calls)  
    public static FrameWorkInterface fwi = null;

RMI 服务器启动(在 清单 4 中是 FrameWorkServer 类)实例化公共内存(类 FrameWorkBase )的基础,并分配一个类域给 FrameWorkBase对象的引用。因为引用是活动的,而且服务器是持久的,所以 JVM 不垃圾收集对象。考虑将启动类字段当作锚点。

因为启动类将对 FrameWorkBase 的引用传递给实现类的构造函数(在 清单 2 中是 FrameWorkImpl 类),而且实现类将 FrameWorkBase 引用保存在它的实例域,所有的 RMI 连接线程都可以访问 FrameWorkBase 类。


既然我们已经为公共内存建立了一个锚,那么我们还需要将其他类链到锚上。用这种方式,任何 RMI 连接线程(在 清单 2中是与实现类关联的线程)都可以找到应用程序线程。

一个 RMI 连接线程使用它的实例域引用来定位 FrameWorkBase 类(参见 清单 8)。然后该 RMI 连接使用 FrameWorkBase 实例域引用,func_tbl ,来访问函数数组(清单 9 中的 FuncHeader 类)。

清单 9. 函数数组
public final class FuncHeader {
  private int nbr_func;         // number of functions
  private FuncDetail[] details; // detail entries

FuncHeader details 数组(相继的被检查)包含一个元素代表每个框架所支持的函数(清单 10 中的 FuncDetail 类)。

清单 10. 函数详细信息
public final class FuncDetail { 
	private String name; // Function name 
	private long used;   // times used 
	private QueueHeader qtbl; // queue

每个 FuncDetail 包含:

  • 函数的字符串名
  • 对函数的队列的引用( 清单 5中的 QueueHeader 类)

每个队列(参见 清单 5)包含:

  • 指向队列的应用程序逻辑类的指针
  • 指向等待列表的指针
  • 队列的可用线程的详细信息数组

通过搜索可用线程的详细信息数组,RMI 连接线程可以找到请求的可用的线程区域( 清单 6 中的 QueueDetail 类)。

每个线程区域( 清单 6 中的 QueueDetail 类)包含指向物理线程的指针。RMI 连接线程使用物理应用程序线程的引用来调用一个同步方法,该方法发出一个对象通知来唤醒应用程序线程。

查找应用程序线程:一个普通的示例
我们已经看到了很多的类和很多的指针 ― 下面还有很多内容。这里用中文做一番描述。

当 client 调用一个远程方法时,RMI 连接线程(将对 FrameWorkBase 类的引用作为实例域)(参见 清单 2):

  • 使用该引用来定位 FrameWorkBase 类
  • 使用 FrameWorkBase 实例域引用来定位函数数组
  • 搜索函数数组来寻找想要的请求的函数详细信息元素
  • 使用函数详细信息队列引用来定位请求的队列(在该示例中是 Queue2)
  • 把请求放进 Queue2 的等待列表
  • 在 Queue2 中找到等待应用程序线程
  • 使用对线程的引用(在本示例中是 Thread2)来 notify() 应用程序线程并等待它的完成

注意:最后一步使用 notify() 。这并不是搞错了。大多数关于线程的书籍都会建议您使用 notifyAll() 。 notify() 和 notifyAll() 方法都是 Object 类的方法。问题是:对于一个特定的 Object ,有多少线程呢?对于 Queue 情况,每个实例仅有一个线程:

QueueThread qt1 = new QueueThread(); QueueThread qt2 = new QueueThread();

Notify() 唤醒一个任意的线程。但 qt1 和 qt2 域是引用。因为每个 QueueThread 对象仅有一个线程,所以 notify() 起作用。它还会唤醒一个任意的线程,但它仅有一个可供选择。

图 3 说明了顺着该链来查找应用程序线程的过程。

图 3. 查找应用程序线程
查找应用程序线程

查找 RMI 连接线程
当应用程序线程处理完了时,应用程序需要查找正调用的 RMI 连接线程以将其唤醒。因为没有返回链,应用程序线程是如何知道谁调用它的呢?它不知道。这就是为什么应用程序线程必须要使用 notifyAll() 以及为什么在 RMI 连接线程要做些额外的工作了。

RMI 连接线程必须向应用程序线程传递些东西以唯一的标识自己。RMI 连接线程所创建和通过引用传递给另一个线程的任何对象对于其它线程来说都是可用的。在一个同步事件(记得与本文有关的 How threads affect common memory)后,然后每个线程都可以访问到对象的当前值了。对于本示例,框架使用了一个整数数组(参见清单 11)。

在 RMI 连接线程为请求找到适当的队列后,它把一个 增强的请求放进队列的等待列表。这是来自参数 FrameWorkParm (参见 清单 3)和清单 11 中的几个其它的域的客户机的对象:

清单 11. 增强的请求中的一些域
	// requestor obj (monitor) to cancel wait
	Object requestor = this;
	
	// integer array created by RMI-Connection thread
	pnp = new int[1];
	pnp[0] = 0;
    	// passed object from the Client
	Object input_from_client = client_data;
    	// returned object from the application thread
	Object back;
  • requestor :为使用对象等待和对象通知方法,RMI 连接线程和应用程序线程都必须可以访问实现类 Object 。
  • pnp :Java 语言通过引用来传递数组。RMI 连接线程将第一个整数设置为 0。当应用程序完成了任务后,它将第一个整数设置为 1。
  • input_from_client :这是在参数(参见 清单 3)中传递给 RMI 服务器的客户机对象。
  • back :由应用程序处理类返回的可选对象。

一个逻辑的预排

当客户机调用一个远程方法时,RMI 连接线程:

  • 通过遍历链来为请求查找恰当的队列
  • 传递给应用程序线程(在队列的等待列表中)足够的信息来告诉 RMI 连接线程如何指出请求完成(通过设置 pnp[0] = 1 )以及notifyAll() 方法的一个引用(requestor)
  • 在队列中查找请求的可用的应用程序线程并唤醒它
  • 等待应用程序线程完成工作(参见清单 12)
  • 当等待完成时,获得从应用程序返回的对象(参见 清单 11)并将对象传回客户机
清单 12. RMI 连接线程等待逻辑
// Wait for the request to complete or timeout
// get the monitor for this RMI object
synchronized (this) {
    // until work finished
    while  (pnp[0] == 0) {
        // wait for a post or timeout
        try {
            // max wait time is the time passed
            wait(time_wait);
        } catch (InterruptedException e) {}
        // When not posted
        if  (pnp[0] == 0) {
            // current time
            time_now = System.currentTimeMillis();
            // decrement wait time
            time_wait -= (time_now - start_time);
            // When no more seconds remain
            if  (time_wait < 1) {
                // get out of the loop, timed out
                break;
            }
            else {
                // new start time
                start_time = time_now;
            }
        }
    }
}

注意:while 循环(在 // until work finished 处)在现在所处的位置是因为当任何应用程序线程发出 notifyAll() 时,Java 语言唤醒 所有的RMI 连接线程。

应用程序线程

另一方面,当处理完成时,应用程序线程做以下的事情(参见清单 13):

  • 获得 RMI 连接的管程(通过在被传递的对象上同步)
  • 将引用赋于来自应用程序处理类的任一返回对象
  • 将等待 RMI 连接线程的整数数组中的第一个整数发出
  • 为所有的 RMI 连接线程发出一个通用的唤醒( notifyAll() )
清单 13. 应用程序线程请求完成活动
// get lock on RMI obj monitor
synchronized (requestor) {
    	// the object from the application processing
    	back = data_object;
    	// set posted
    	pnp[0] = 1;
    	// wake up all RMI-Connection threads
    	requestor.notifyAll();
}

自主的请求

自主请求的事件流程和计时请求的事件流程是一样的,除了自主请求没有要求 RMI 连接线程等待完成外。在唤醒了应用程序线程后,RMI 连接线程返回给客户机一个“它已经被调度”的消息。自主请求作为结果看起来也许很简单,但是存在一个隐藏的困难。

来自应用程序处理类( 清单 7)的 doWork() 方法的返回数据会发生些什么呢?它的返回数据到哪里去了并不该作为应用程序关注的事。开发者必须能够为计时的或是自主的请求使用相同的应用程序逻辑。所以,我们需要一个自主请求的代理。

代理仅是另一个逻辑进程。换句话说,它是个有关联线程的队列。普通队列和代理队列的唯一不同在于 RMI 连接线程访问普通队列,而应用程序线程 可选的访问代理队列。

代理

对于一个计时请求来说,客户机从应用程序获取响应。将代理考虑为一个服务器端的伪客户机:该代理从应用程序获取响应。 这有点迷惑,但先放在那;最后这点会变清楚的。当进入多部分请求时,您将发现没有代理是无法存活的。

应用程序处理类( doWork() 方法)可以返回一个对象到应用程序线程。对于自主请求来说,当想要时,应用程序线程可以通过创建一个新的增强的请求(用刚返回的对象)、将增强的请求放进代理的等待列表并唤醒一个代理线程来激活一个代理程序逻辑进程。这与 RMI 连接线程在根本上是相同的。

代理逻辑进程异步的完成,并且没有任何返回数据。代理队列应用程序处理类是放向后调用、向前调用或处理请求完成的任何其它的必要逻辑的地方。

监控异步进程

任何异步进程的一个额外的关键要求是监控逻辑进程的方式。因为同步请求可能会超时,所以需要有一个释放内存的过程。自主请求执行无须客户机等待它们的完成;当自主请求不能完成时 ― 换句话说,当它们停止时 ― 很难检测出该停止。

Daemon(守护)线程

如果您使用 UNIX 类型的操作系统,那么您应该熟悉daemon 线程这个术语。Windows 类型的操作系统将其称为 terminate-and-stay(终止并存留)线程。该线程起作用然后休眠直到下一次它被请求。

监控环境的一种方式是使用一个定期扫描环境的 daemon 线程。把一个线程称为 daemon仅仅是意味着它不是特定的应用程序或 RMI 连接的一部分。当监控线程发现问题时,它可以改正此问题,记录该问题的详细信息、发送一个消息给中间件队列或内部的通知另一个远程对象。该行为取决于应用程序,所以超出了本文的范围。

关闭

您需要一个彻底的方式来关闭服务器而不要管正在处理的请求。因为RMI 运行时包含从不结束的线程,所以结束 RMI 服务器的唯一的方式是计划性的使用一个系统退出方法或是使用一个操作系统清除。后者是最粗野的,而且要求手工的干预。

关闭 RMI 服务器的得体的方式是用一个关闭方法。但是,如果关闭方法仅结束 Java Virtual Machine,那么该方法的返回消息从不会返回客户机。更好的方式是启动一个关闭线程。该线程休眠大约两秒的时间来给线程的返回消息一个清除虚拟机的机会,然后发出 System.exit(0) 。

到目前为止我们已经介绍了什么呢?

也许冗长而难读吧。现在,在设法消化所有的这些信息之前,我们需要提出每个项目的三个重要的问题:

  1. 框架的优点是什么呢?
  2. 它是否完成了它的承诺呢?
  3. 由于技术原因,对它来说是否存在技术以外的东西呢?

为回答第一个问题,框架为我们提供了能力:

  • 来计时 RMI 服务器请求。
  • 来运行自主的 RMI 服务器请求。
  • 把代理作为任何自主请求的一部分来运行。
  • 来限制应用程序线程常见的创建/销毁开销。
  • 来减少线程过载问题。
  • 来运行来自所有应用程序的递归请求。
  • 来简便的调试请求,尤其是自主请求。(我们知道工作所驻留的应用程序线程和应用程序类。无须捕获无关的代码。)
  • 使用管程来找出未执行的请求。
  • 得体的关闭 RMI 服务器。

框架将 RMI 线程环境从应用程序线程环境分离。另外,它还把应用程序线程从实际的应用程序逻辑分离。这是在面向对象设计中的非常重要的更高级的抽象

它是否完成了它的承诺呢?正如您将看到的在与本文一起的 zip 文件(从 参考资料部分下载)中所包含的代码,框架表现的极好。

第三个问题的答案,“哦,这很好,但……”也许投入所能得到的回报并不值得付出努力。结构的、关键的部分是错误恢复。有时非常规代码远比标准代码更重要。框架所需的是存在的更充分的理由。

如果我们能扩展这个简单的框架的话,由什么来支持每个请求的多队列呢?当一个客户机请求对资源的多访问时,如果我们能分割该请求,并把每个组件放进它自己的队列,那么我们就可以并行处理请求了。现在,这个可能性是无尽的。这是 请求代理(request brokering),它是下一节的主题。

并行处理多部分请求

一旦我们建立了基本的队列环境,马上就很明显,一些请求确实是包含了多个行为或组件。例如,完成请求也许会要求访问两个不同的数据库。我们可以以线性方式来访问每个数据库,但是对第二个的访问要等到第一个完成后才行。

请求代理

在介绍逻辑进程时,我们使用了从 Amazon.com 订购书或盒式录像带的类比。用多部分的请求,书和盒式录像带我们一次就可以 订购到。书部分的请求进入书队列,盒式录像带部分进入盒式录像带队列。两个队列都有自己的雇员(应用程序线程)来处理定单。

处理多行为请求的更好些的方式是把请求分割成它的组件部件并把每个组件放入一个单独的队列。这样就可以并行处理了。这比线性编程要难,但是得到的益处远比预先的额外工作要重要。

我们需要什么来支持请求代理呢?我们要理解客户机请求不再是仅被单一的逻辑进程所关注的。所以,我们必须把客户机请求放进一个通用的区域,从而任何数量的逻辑进程都可以访问它。由于我们已经有了一个公共内存环境,所以我们现在必须将它增强。

这是个简单的部分。困难的工作一直是建立结构中第一个基础的块。在接下来的几节中,我们通过以下几种方式来增强单组件框架:

  • 为计时和自主请求创建单独的增强请求类
  • 添加一个用于放置被停止的自主请求的类
  • 增强函数描述类
  • 为公共内存基础类添加额外的类

然后我们用新的框架来大致处理一个简单的请求。

增强公共内存

我们需要一个公共的地方来放置来自客户机的同步请求和异步请求的详细信息。该公共信息包括:

  • 来自客户机的对象(在 清单 3中是 FrameWorkParm 类的 client_data 对象)
  • 从每个应用程序处理类返回的对象(参见 清单 5),因为我们不再有一个单一的组件结构
  • 所有的其它增强请求域

所需的全部就是一个简单的对象数组。本例中的对象是 SyncDetail 类 AsyncDetail 类(参见清单 14)。

我们还需要把这两个类添加到公共内存的基础 FrameWorkBase 类。

清单 14. 增强的请求类
public final class SyncDetail {
	private Object[] output;     // output data array
    	private Object   input;      // client input data, if any
    	private int   status;        // 0=avail 1=busy
    	private int   nbr_que;       // total queue's in function
    	private int   nbr_remaining; // remaining to be processed
    	private int   wait_time;     // max wait time in seconds 
    	private Object requestor;    // requestor obj to cancel wait
    	private int[] pnp;           // 0 not posted, 1 is posted
    public final class AsyncDetail {
    	private Object input;          // client input
    	private QueueHeader out_agent; // agent queue, if any
    	private int   nbr_que;         // nbr of queues in function
    	private int   nbr_remaining;   // remaining unprocessed
    	private int   status;          // 0 = available, 1 = busy
    	private Object[] output;       // output array

这些对象所驻留的数组是个链表。该框架中的所有的动态数组都是链表。对链表条目的访问是直接通过下标。当一个线程将一个对象放进任何链表时,该线程必须传给另一个对象的全部东西就是原整数(下标)。另外,在大量的使用中,扩展一个链表是很简单的 ― 在原来的链表上再链上一个新的链表。

为异步请求保存信息的另一个公共的地方是那些已停止的请求的动态数组。当同步请求花费了比用户可以等待时间的更长时,请求端的连接就终止了。当一个异步请求所花费的时间多于允许的,处理可能就不能完成、请求可能就停止了。为了从停止状态恢复,就必须有一个地方来放置信息。这个地方就是 StallDetail 类(参见清单 15)。

我们还需要把这个类加到公共内存的基础,即 FrameWorkBase 类。

清单 15. 停止的元素类
public final class StallDetail {
	private long entered;   // time entered
	private int gen_name; // AsyncDetail subscript
	private int status;       // 0 = available, 1 = busy private
	int failed_reason;      // why it is here

告诉服务器关于请求的部分

我们讨论了为多组件请求结构化环境的问题,但是这还不能作为 RMI 服务器如何知道什么组件是请求的部分的证据。

组件结构是开发者从开始就知道的信息。在基本的框架中,每个函数(逻辑进程的名称)都有单一的队列。在请求代理框架,每个函数有一个队列列表。与每个 函数相联的 队列的列表是组件结构。这是被改变了的 FuncDetail 类(参见清单 16)。当您编码自己的系统时,而不是使用演示系统,您将根据需要为每个函数建立一个结构。

清单 16. 增强的函数类
public final class FuncDetail {
  	private String name;        // Function name
   	private long   used;        // times used
   	private QueueHeader agent;  // optional agent queue
   	private int    nbr_que;     // number of queues in this entry
   	private QueueHeader[] qtbl; // array of queues

从一个简单的公共内存环境开始,到现在我们已经通过增加几个类和修改其它的类(包括增加新的类到 FrameWorkBase 类)增强了该环境来支持请求代理。图 4 是支持该框架的最终公共内存结构。

图 4.公共内存引用
公共内存引用

增强的框架逻辑处理过程

对于任何的请求,RMI 连接线程:

  • 用队列的列表和对 client_data 的引用(参见 清单 3)来创建一个 AsyncDetail 对象或是一个 SyncDetail 对象
  • 通过遍历链来为请求查找恰当的队列
  • 将代表 AsyncDetail 或 SyncDetail 对象的整数下标根据优先级放进函数的每个队列的列表中
  • 唤醒每个队列的应用程序线程

对于同步请求,RMI 连接线程等待直到所有的队列完成了处理。然后该框架将来自所有逻辑进程的对象连成一个单一的对象数组并把对象数组返回客户机。

对于自主请求,RMI 连接线程返回客户机一个“已经被调度”的消息。处理是异步发生的。当最后一个队列的应用程序完成了处理,应用程序线程将来自所有的逻辑进程的返回对象 选择的连成一个单一的对象数组,并激活一个新的逻辑进程,即代理。代理应用程序可以检查那些返回对象并为支持请求而采取进一步的行动。例如,如果所有的逻辑进程都正常完成了,那么它将发出一个提交;否则,它将发出一个回滚。

看它的工作情况

我们已经讨论了很多关于线程和队列的东西了,也许还有点迷惑。没有什么能够象观看异步进程管理器运行更能减轻迷惑的了。现在到了把所有的东西放在一起演示的时候了。如果您还没有 下载 zip 文件,那么请现在就下载。

该演示要求至少是 Java 平台 1.1 版本。将文件解压缩到一个目录。结构如下:

  • /Doc:包含一个文件: Doc.html ,该文件为所有的类和运行时过程提供了文档
  • /Source:包含本文的所有的源代码
  • /Classes:包含本文的所有的类,包括用于安全性的 policy.all 文件
FrameWorkServer

跟随启动一个单一访问的客户机(DemoClient_3,其函数是 F3)的指导,此客户机由三个队列组成(是命名为"第一次"的节)。

运行时摘要

这是当你启动客户机时所发生的。客户机以 FrameWorkParm 对象作为参数调用远程对象 FrameWorkServer 的 syncRequest() 方法。syncRequest() :

  • 找到请求的函数(命名为 F3),它包含三个队列,分别叫做 Q2、Q2 和 Q3。
  • 创建一个 SyncDetail 对象,并把它的下标符号放进队列 Q1、Q2 和 Q3 的等待列表中。
  • 发现 Q1 没有活跃的线程,所以初始化一个新的线程 *
  • 发现 Q2 没有活跃的线程,所以初始化一个新的线程 *
  • 发现 Q3 没有活跃的线程,所以初始化一个新的线程 *
  • 等待所有的逻辑进程完成了处理
  • 当被通知完成时,从每个逻辑进程获得返回对象,把每个对象连成一个对象数组,并把对象数组返回给客户机。

注意:在标有 * 的步骤中,如果已经有了活跃的线程,那么 syncRequest() 所有做的就仅是 notify() 它。

当 syncRequest() 在等待时, 每个应用程序线程:

  • 搜索等待列表来找第一个可用的请求
  • 为请求获得 SyncDetail 对象
  • 调用应用程序处理类的 doWork() 方法来执行队列的逻辑
  • 保存来自应用程序处理类的返回对象的引用到 SyncDetail 类中
  • 当它确定所有其它队列已完成了处理时,唤醒正在等待的 RMI 连接线程
  • 搜索等待列表来查找第一个可用的请求,因为没有找到,并发出一个 wait() 直到下一个 notify() 。

装入

当许多客户机同时的命中服务器时,令人兴奋的事情出现了!另外,没有可视化的工具的话,您就没有办法知道正在发生些什么。在本包中,有两个类正是提供了这样的一个工具。(即,命名为“装入”的指导部分。)

跟随指导来运行该可视化工具, FrameWorkThreads 类。

跟随指导来运行多客户机线程, DemoClientMultiBegin 类,来跟随系统加载。

关闭

在您处理好了服务器后,您可以用一个客户机请求, DemoClient_Shutdown ,得体的关闭它。

框架的可能的增强

在本篇简洁的文章中,我们只能研究一下一个异步进程管理器的骨架。我们用 框架(framework)这个词是因为这个词描述了一个骨架支持结构。一些可以补充该支持的元素有:

  • 错误恢复:如我们上面所提到的,有时非常规的代码远比标准代码更重要。对于一个定制的框架,错误恢复取决于应用程序。大多数检测取决进程的计时的不同方面。捕获一个异常是很简单的;但找出出轨的线程却很难。要知道寻找什么,您必须知道应用程序是做什么的。
  • 阈值:何时实例化或激活一个应用程序线程的问题是最重要的。该代码示例目前所表现的方式,框架在一个逻辑进程中实例化或激活一个新的线程的唯一的时间是当队列中没有活跃的线程或是当一个新的请求进入一个等待列表引起溢出到下一个列表时。通常在队列的负载变的比用户决定的值大时,激活另一个线程会更好些。这就是 阈值处理。当 RMI 连接线程把一个新的请求放入等待列表时,线程会能够确定该队列目前的负载(根据预先确定的要求),而且会启动或激活另一个应用程序线程。
  • Hook 和出口:开发者如何处理连接池呢?开发者如何处理消息队列中间件包呢?记住,服务器是持久的。您可以添加一个“启动(start-up)hook”,使用它您建立用于保存那些产品的实例化的类和私有线程的单独的内存区域。您可以添加一个用于得体的关闭单独的区域的“关闭 (shutdown) hook”。
  • 日志记录:任何曾经用后台进程(background process)的人都知道记录错误是多么的重要。发生故障后,还能怎么知道发生了什么呢?任何普通目的的日志就可以满足要求了。现在有可用的商业产品,而且标准的 Java 语言不久的将来也会支持记录。另外请参见开放源代码工程 Log4j、Nate Sammons 的关于系统日志(Syslog)的文章以及 alphaWorks Logging Toolkit for Java,所有这些都在 参考资料中。
  • 定制对通用:这是个定制的框架。您建立这样一个系统来支持应用程序的特定的集合。当您需要支持广泛一系列的应用程序或没有时间来自己设计一个的时候,那么比较好的选择就是购买一个通用的、功能完全的异步进程管理器。参见 参考资料的一般框架的一个列表。

结论

本文讲了很多的内容。没有人宣称建立一个后端框架是简单的。记住,Java 设计师在建立 EJB 和 GUI 容器上投入了非常多的努力。现在我们有什么呢?

我们将 RMI 逻辑从应用程序逻辑分离出来。通过这样做,我们开辟了一个应用程序队列和线程(并不局限于 RMI)的世界。这个世界使我们能:

  • 让 RMI 连接线程和应用程序线程可以彼此对话
  • 定时请求
  • 无需用应用程序线程来使服务器过载就可以运行自主请求
  • 将代理作为任何自主请求的一部分来运行
  • 处理来自队列的等待列表的多请求,从而减少了应用程序启动/停止的开销
  • 通过保持每个事件的计数来调优服务器
  • 控制创建/销毁线程所带来的开销
  • 简单的作为应用程序线程的主题插入任何应用程序类
  • 轻松的捕获线程或应用程序类进行调试
  • 运行来自任何应用程序的递归请求
  • 使用一个管程来找到未执行的请求,并用一个方法来处理它们
  • 得体的关闭 RMI 服务器

然后我们把单处理环境增强为可以并行处理的请求代理。我们丰富了公共内存环境,从而:

  • 运行并行队列处理(请求代理)
  • 容易的记录事件
  • 增加几乎所有的中间件产品,使其作为出口或 hook
  • 完全为任何应用程序定制代码

从这点,RMI 服务器应用程序容器不再是空的了。

参考资料



本文转载自:http://www.ibm.com/developerworks/cn/java/j-rmiframe/
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值