关于解决Java编程语言线程问题的建议
 

2010-04-27 作者:IBM developerworks网站 来源:IBM developerworks网站

 

Java 语言的线程模型是此语言的一个最难另人满意的部分。尽管 Java 语言本身就支持线程编程是件好事,但是它对线程的语法和类包的支持太少,只能适用于极小型的应用环境。

task(任务)的概念

Java 线程模型的根本问题是它完全不是面向对象的。面向对象 (OO) 设计人员根本不按线程角度考虑问题;他们考虑的是 同步信息 异步 信息(同步信息被立即处理 -- 直到信息处理完成才返回消息句柄;异步信息收到后将在后台处理一段时间 -- 而早在信息处理结束前就返回消息句柄)。Java 编程语言中的 Toolkit.getImage() 方法就是异步信息的一个好例子。 getImage() 的消息句柄将被立即返回,而不必等到整个图像被后台线程取回。

这是面向对象 (OO) 的处理方法。但是,如前所述,Java 的线程模型是非面向对象的。一个 Java 编程语言线程实际上只是一个 run() 过程,它调用了其它的过程。在这里就根本没有对象、异步或同步信息以及其它概念。

对于此问题,在我的书中深入讨论过的一个解决方法是,使用一个 Active_object。 active 对象是可以接收异步请求的对象,它在接收到请求后的一段时间内以后台方式得以处理。在 Java 编程语言中,一个请求可被封装在一个对象中。例如,你可以把一个通过 Runnable 接口实现的实例传送给此 active 对象,该接口的 run() 方法封装了需要完成的工作。该 runnable 对象被此 active 对象排入到队列中,当轮到它执行时,active 对象使用一个后台线程来执行它。

在一个 active 对象上运行的异步信息实际上是同步的,因为它们被一个单一的服务线程按顺序从队列中取出并执行。因此,使用一个 active 对象以一种更为过程化的模型可以消除大多数的同步问题。

在某种意义上,Java 编程语言的整个 Swing/AWT 子系统是一个 active 对象。向一个 Swing 队列传送一条讯息的唯一安全的途径是,调用一个类似 SwingUtilities.invokeLater() 的方法,这样就在 Swing 事件队列上发送了一个 runnable 对象,当轮到它执行时, Swing 事件处理线程将会处理它。

那么我的第一个建议是,向 Java 编程语言中加入一个 task (任务)的概念,从而将active 对象集成到语言中。( task的概念是从 Intel 的 RMX 操作系统和 Ada 编程语言借鉴过来的。大多数实时操作系统都支持类似的概念。)

一个任务有一个内置的 active 对象分发程序,并自动管理那些处理异步信息的全部机制。

定义一个任务和定义一个类基本相同,不同的只是需要在任务的方法前加一个 asynchronous 修饰符来指示 active 对象的分配程序在后台处理这些方法。请参考我的书中第九章的基于类方法,再看以下的 file_io 类,它使用了在《 Taming Java Threads 》中所讨论的 Active_object 类来实现异步写操作:

1             
2         interface Exception_handler
3                 
4         {   void handle_exception( Throwable e ); }
5         class File_io_task {   Active_object dispatcher = new Active_object();
6         final OutputStream      file;
7         final Exception_handler handler;
8         File_io_task( String file_name, Exception_handler handler )
9                                                 throws IOException
10         {   file = new FileOutputStream( file_name );
11             this.handler = handler;
12         }
13         public void write( final byte[] bytes ) {
14             // The following call asks the active-object dispatcher
15             // to enqueue the Runnable object on its request
16             // queue. A thread associated with the active object
17             // dequeues the runnable objects and executes them
18             // one at a time.
19             dispatcher.dispatch
20             (   new Runnable()
21         {   public void run() {
22                         try
23                         {   byte[] copy new byte[ bytes.length ];
24                             System.arrayCopy(   bytes,  0,
25                                                 copy,   0,
26                                                 bytes.length );
27                             file.write( copy );
28                         }
29                         catch( Throwable problem )
30                         {   handler.handle_exception( problem );
31                         }
32                     }
33                 }
34             );
35         }
36     }
37

所有的写请求都用一个 dispatch() 过程调用被放在 active-object 的输入队列中排队。在后台处理这些异步信息时出现的任何异常 (exception) 都由 Exception_handler 对象处理,此 Exception_handler 对象被传送到 File_io_task 的构造函数中。您要写内容到文件时,代码如下:

1 File_io_task io =   new File_io_task
2                         ( "foo.txt"
3                             new Exception_handler
4         {   public void handle( Throwable e ) {   e.printStackTrace();
5                                 }
6                             }
7                         );
8     //...
9     io.write( some_bytes );
10

这种基于类的处理方法,其主要问题是太复杂了 -- 对于一个这样简单的操作,代码太杂了。向 Java 语言引入 $task 和 $asynchronous 关键字后,就可以按下面这样重写以前的代码:

1 $task File_io $error{ $.printStackTrace(); }
2     {
3         OutputStream file;
4         File_io( String file_name ) throws IOException
5         {   file = new FileOutputStream( file_name );
6         }
7         asynchronous public write( byte[] bytes )
8         {   file.write( bytes );
9         }
10     }
11

注意,异步方法并没有指定返回值,因为其句柄将被立即返回,而不用等到请求的操作处理完成后。所以,此时没有合理的返回值。对于派生出的模型, $task 关键字和 class 一样同效: $task 可以实现接口、继承类和继承的其它任务。标有 asynchronous 关键字的方法由 $task 在后台处理。其它的方法将同步运行,就像在类中一样。

$task 关键字可以用一个可选的 $error 从句修饰 (如上所示),它表明对任何无法被异步方法本身捕捉的异常将有一个缺省的处理程序。我使用 $ 来代表被抛出的异常对象。如果没有指定 $error 从句,就将打印出一个合理的出错信息(很可能是堆栈跟踪信息)。

注意,为确保线程安全,异步方法的参数必须是不变 (immutable) 的。运行时系统应通过相关语义来保证这种不变性(简单的复制通常是不够的)。

所有的 task 对象必须支持一些伪信息 (pseudo-message),例如:

1 some_task.close()  在此调用后发送的任何异步信息都产生一个 TaskClosedException 。但是,在 active 对象队列上等候的消息仍能被提供。  
2 some_task.join()  调用程序被阻断,直到此任务关闭、而且所有未完成的请求都被处理完毕。
3

除了常用的修饰符( public 等), task 关键字还应接受一个 $pooled(n) 修饰符,它导致 task 使用一个线程池,而不是使用单个线程来运行异步请求。 n 指定了所需线程池的大小;必要时,此线程池可以增加,但是当不再需要线程时,它应该缩到原来的大小。伪域 (pseudo-field) $pool_size 返回在 $pooled(n) 中指定的原始 n 参数值。

在《 Taming Java Threads 》的第八章中,我给出了一个服务器端的 socket 处理程序,作为线程池的例子。它是关于使用线程池的任务的一个好例子。其基本思路是产生一个独立对象,它的任务是监控一个服务器端的 socket。每当一个客户机连接到服务器时,服务器端的对象会从池中抓取一个预先创建的睡眠线程,并把此线程设置为服务于客户端连接。socket 服务器会产出一个额外的客户服务线程,但是当连接关闭时,这些额外的线程将被删除。实现 socket 服务器的推荐语法如下:

1 public $pooled(10) $task Client_handler {
2         PrintWriter log = new PrintWriter( System.out );
3         public asynchronous void handle( Socket connection_to_the_client ) {  
4             log.println("writing");
5             // client-handling code goes here. Every call to
6             // handle()  is executed on its own thread, but 10
7             // threads are pre-created for this purpose. Additional
8             // threads are created on an as-needed basis, but are
9             // discarded when handle() returns.
10         }
11     }  
12     $task Socket_server
13     {
14         ServerSocket server;
15         Client_handler client_handlers = new Client_handler();
16         public Socket_server( int port_number ) {   server = new ServerSocket(port_number);
17         }
18         public $asynchronous listen(Client_handler client) {  
19             // This method is executed on its own thread.
20             while( true )
21             {   client_handlers.handle( server.accept() );
22             }
23         }
24     }
25     //...
26     Socket_server = new Socket_server( the_port_number );
27     server.listen()
28

Socket_server 对象使用一个独立的后台线程处理异步的 listen() 请求,它封装 socket 的“接受”循环。当每个客户端连接时, listen() 请求一个 Client_handler 通过调用 handle() 来处理请求。每个 handle() 请求在它们自己的线程中执行(因为这是一个 $pooled 任务)。

注意,每个传送到 $pooled $task 的异步消息实际上都使用它们自己的线程来处理。典型情况下,由于一个 $pooled $task 用于实现一个自主操作;所以对于解决与访问状态变量有关的潜在的同步问题,最好的解决方法是在 $asynchronous 方法中使用 this 是指向的对象的一个独有副本。这就是说,当向一个 $pooled $task 发送一个异步请求时,将执行一个 clone() 操作,并且此方法的 this 指针会指向此克隆对象。线程之间的通信可通过对 static 区的同步访问实现。

 

 



Java 中的中文编码问题
Java基础知识的三十个经典问答
玩转 Java Web 应用开发
使用Spring更好地处理Struts
用Eclipse开发iPhone Web应用
插件系统框架分析
更多...   


Struts+Spring+Hibernate
基于J2EE的Web 2.0应用开发
J2EE设计模式和性能调优
Java EE 5企业级架构设计
Java单元测试方法与技术
Java编程方法与技术


Struts+Spring+Hibernate/EJB+性能优化
华夏基金 ActiveMQ 原理与管理
某民航公司 Java基础编程到应用开发
某风电公司 Java 应用开发平台与迁移
日照港 J2EE应用开发技术框架与实践
某跨国公司 工作流管理JBPM
东方航空公司 高级J2EE及其前沿技术
更多...   
 
 
 
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号