快精灵印艺坊 您身边的文印专家
广州名片 深圳名片 会员卡 贵宾卡 印刷 设计教程
产品展示 在线订购 会员中心 产品模板 设计指南 在线编辑
 首页 名片设计   CorelDRAW   Illustrator   AuotoCAD   Painter   其他软件   Photoshop   Fireworks   Flash  

 » 彩色名片
 » PVC卡
 » 彩色磁性卡
 » 彩页/画册
 » 个性印务
 » 彩色不干胶
 » 明信片
   » 明信片
   » 彩色书签
   » 门挂
 » 其他产品与服务
   » 创业锦囊
   » 办公用品
     » 信封、信纸
     » 便签纸、斜面纸砖
     » 无碳复印纸
   » 海报
   » 大篇幅印刷
     » KT板
     » 海报
     » 横幅

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);
  }
  }
  }




返回类别: 教程
上一教程: 用了12个小时完成一个计算器小作业
下一教程: 诊断和纠正 Java 程序中反复出现的错误类型

您可以阅读与"Java I/O API之性能分析 (上)"相关的教程:
· Java I/O API之性能分析
· Java I/O API之性能分析 (下)
· Java API的Date, Calendar日期处理相关类分析
· JAVA程序的性能优化
· Java性能
    微笑服务 优质保证 索取样品