gembin

OSGi, Eclipse Equinox, ECF, Virgo, Gemini, Apache Felix, Karaf, Aires, Camel, Eclipse RCP

HBase, Hadoop, ZooKeeper, Cassandra

Flex4, AS3, Swiz framework, GraniteDS, BlazeDS etc.

There is nothing that software can't fix. Unfortunately, there is also nothing that software can't completely fuck up. That gap is called talent.

About Me

 

Java I/O API之性能分析

IO API的可伸缩性对Web应用有着极其重要的意义。Java 1.4版以前的API中,阻塞I/O令许多人失望。从J2SE 1.4版本开始,Java终于有了可伸缩的I/O API。本文分析并计算了新旧I/O API在可伸缩性方面的差异。

  一、概述

IO API的可伸缩性对Web应用有着极其重要的意义。Java 1.4版以前的API中,阻塞I/O令许多人失望。从J2SE 1.4版本开始,Java终于有了可伸缩的I/O API。本文分析并计算了新旧IO API在可伸缩性方面的差异。Java向Socket写入数据时必须调用关联的OutputStream的write()方法。只有当所有的数据全部写入 时,write()方法调用才会返回。倘若发送缓冲区已满且连接速度很低,这个调用可能需要一段时间才能完成。如果程序只使用单一的线程,其他连接就必须 等待,即使那些连接已经做好了调用write()的准备也一样。为了解决这个问题,你必须把每一个Socket和一个线程关联起来;采用这种方法之后,当 一个线程由于I/O相关的任务被阻塞时,另一个线程仍旧能够运行。

  尽管线程的开销不如进程那么大,但是,考虑到底层的操作平台,线 程和进程都属于消耗大量资源的程序结构。每一个线程都要占用一定数量的内存,而且除此之外,多个线程还意味着线程上下文的切换,而这种切换也需要昂贵的资 源开销。因此,Java需要一个新的API来分离Socket与线程之间过于紧密的联系。在新的Java I/O API(java.nio.*)中,这个目标终于实现了。

  本文分析和比较了用新、旧两种I/O API编写的简单Web服务器。由于作为Web协议的HTTP不再象原来那样只用于一些简单的目的,因此这里介绍的例子只包含关键的功能,或者说,它们既不考虑安全因素,也不严格遵从协议规范。

  二、用旧API编写的HTTP服务器

  首先我们来看看用旧式API编写的HTTP服务器。这个实现只使用了一个类。main()方法首先创建了一个绑定到8080端口的ServerSocket:


  public static void main() throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
for (int i=0; i < Integer.parseInt(args[0]); i++) {
new Httpd(serverSocket);
}
}

接下来,main()方法创建了一系列的Httpd对象,并用共享的ServerSocket初始化它们。在Httpd的构造函数中,我们保证每一个实 例都有一个有意义的名字,设置默认协议,然后通过调用其超类Thread的start()方法启动服务器。此举导致对run()方法的一次异步调用,而 run()方法包含一个无限循环。

  在run()方法的无限循环中,ServerSocket的阻塞性accpet()方法被调用。 当客户程序连接服务器的8080端口,accept()方法将返回一个Socket对象。每一个Socket关联着一个InputStream和一个 OutputStream,两者都要在后继的handleRequest()方法调用中用到。这个方法将读取客户程序的请求,经过检查和处理,然后把合适 的应答发送给客户程序。如果客户程序的请求合法,通过sendFile()方法返回客户程序请求的文件;否则,客户程序将收到相应的错误信息(调用 sendError())方法。


  while (true) {
...
socket = serverSocket.accept();
...
handleRequest();
...
socket.close();
}

现在我们来分析一下这个实现。它能够出色地完成任务吗?答案基本上是肯定的。当然,请求分析过程还可以进一步优化,因为在性能方面 StringTokenizer的声誉一直不佳。但这个程序至少已经关闭了TCP延迟(对于短暂的连接来说它很不合适),同时为外发的文件设置了缓冲。而 且更重要的是,所有的线程操作都相互独立。新的连接请求由哪一个线程处理由本机的(因而也是速度较快的)accept()方法决定。除了 ServerSocket对象之外,各个线程之间不共享可能需要同步的任何其他资源。这个方案速度较快,但令人遗憾的是,它不具有很好的可伸缩性,其原因 就在于,很显然地,线程是一种有限的资源。

  三、非阻塞的HTTP服务器

  下面我们来看看另一个使用非阻塞的新I/O API的方案。新的方案要比原来的方案稍微复杂一点,而且它需要各个线程的协作。它包含下面四个类:

  ·NIOHttpd
·Acceptor
·Connection
·ConnectionSelector


NIOHttpd的主要任务是启动服务器。就象前面的Httpd一样,一个服务器Socket被绑定到8080端口。两者主要的区别在于,新版本的 服务器使用java.nio.channels.ServerSocketChannel而不是ServerSocket。在利用bind()方法显式地 把Socket绑定到端口之前,必须先打开一个管道(Channel)。然后,main()方法实例化了一个ConnectionSelector和一个 Acceptor。这样,每一个ConnectionSelector都可以用一个Acceptor注册;另外,实例化Acceptor时还提供了 ServerSocketChannel。


  public static void main() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(8080));
ConnectionSelector cs = new ConnectionSelector();
new Acceptor(ssc, cs);
}

为了理解这两个线程之间的交互过程,首先我们来仔细地分析一下Acceptor。Acceptor的主要任务是接受传入的连接请求,并通过 ConnectionSelector注册它们。Acceptor的构造函数调用了超类的start()方法;run()方法包含了必需的无限循环。在这 个循环中,一个阻塞性的accept()方法被调用,它最终将返回一个Socket对象——这个过程几乎与Httpd的处理过程一样,但这里使用的是 ServerSocketChannel的accept()方法,而不是ServerSocket的accept()方法。最后,以调用accept() 方法获得的socketChannel对象为参数创建一个Connection对象,并通过ConnectionSelector的queue()方法注 册它。


  while (true) {
...
socketChannel = serverSocketChannel.accept();
connectionSelector.queue(new Connection(socketChannel));
...
}

总而言之:Acceptor只能在一个无限循环中接受连接请求和通过ConnectionSelector注册连接。与Acceptor一 样,ConnectionSelector也是一个线程。在构造函数中,它构造了一个队列,并用Selector.open()方法打开了一个 java.nio.channels.Selector。Selector是整个服务器中最重要的部分之一,它使得程序能够注册连接,能够获取已经允许读 取和写入操作的连接的清单。

  构造函数调用start()方法之后,run()方法里面的无限循环开始执行。在这个循环中,程序调用了Selector的select()方法。这个方法一直阻塞,直到已经注册的连接之一做好了I/O操作的准备,或Selector的wakeup()方法被调用。


  while (true) {
...
int i = selector.select();
registerQueuedConnections();
...
// 处理连接...
}

当ConnectionSelector线程执行select()时,没有一个Acceptor线程能够用该Selector注册连接,因为对应的方法是同步方法,理解这一点是很重要的。因此这里使用了队列,必要时Acceptor线程向队列加入连接。


  public void queue(Connection connection) {
synchronized (queue) {
queue.add(connection);
}
selector.wakeup();
}

紧接着把连接放入队列的操作,Acceptor调用Selector的wakeup()方法。这个调用导致ConnectionSelector线程继 续执行,从正在被阻塞的select()调用返回。由于Selector不再被阻塞,ConnectionSelector现在能够从队列注册连接。在 registerQueuedConnections()方法中,其实施过程如下:


  if (!queue.isEmpty()) {
synchronized (queue) {
while (!queue.isEmpty()) {
Connection connection =
(Connection)queue.remove(queue.size()-1);
connection.register(selector);
}
}
}

posted on 2008-04-11 10:28 gembin 阅读(468) 评论(0)  编辑  收藏 所属分类: JavaSE


只有注册用户登录后才能发表评论。


网站导航:
 

导航

统计

常用链接

留言簿(6)

随笔分类(440)

随笔档案(378)

文章档案(6)

新闻档案(1)

相册

收藏夹(9)

Adobe

Android

AS3

Blog-Links

Build

Design Pattern

Eclipse

Favorite Links

Flickr

Game Dev

HBase

Identity Management

IT resources

JEE

Language

OpenID

OSGi

SOA

Version Control

最新随笔

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜

free counters