Merlin 给 Java 平台带来了非阻塞 I/O

新增的功能大幅降低了线程开销

Java 技术平台早就应该提供非阻塞 I/O 机制了。幸运的是,Merlin(JDK 1.4)有一根几乎在各个场合都适用的魔杖,而解除阻塞了的 I/O 的阻塞状态正是这位魔术师的专长。软件工程师 Aruna Kalagnanam 和 Balu G 介绍了 Merlin 的新 I/O 包 ― java.nio(NIO)― 的这种非阻塞功能,并且用一个套接字编程示例向您展示 NIO 能做些什么。请单击本文顶部或底部的 讨论,在 讨论论坛与作者及其他读者分享您关于本文的心得。

0 评论:

Aruna Kalagnanam (kaaruna@in.ibm.com), 软件工程师, IBM

Balu G (gbalu@in.ibm.com), 软件工程师, IBM

2002 年 3 月 12 日

  • 内容

在 IBM Bluemix 云平台上开发并部署您的下一个应用。

开始您的试用

服务器在合理的时间之内处理大量客户机请求的能力取决于服务器使用 I/O 流的效率。同时为成百上千个客户机提供服务的服务器必须能够并发地使用 I/O 服务。Java 平台直到 JDK 1.4(也就是 Merlin)才支持非阻塞 I/O 调用。用 Java 语言写的服务器,由于其线程与客户机之比几乎是一比一,因而易于受到大量线程开销的影响,其结果是既导致了性能问题又缺乏可伸缩性。

为了解决这个问题,Java 平台的最新发行版引入了一组新的类。Merlin 的 java.nio 包充满了解决线程开销问题的技巧,包中最重要的是新的 SelectableChannel 类和 Selector 类。 通道(channel)是客户机和服务器之间的一种通信方式。 选择器(selector)与 Windows 消息循环类似,它从不同客户机捕获各种事件并将它们分派到相应的事件处理程序。在本文,我们将向您展示这两个类如何协同工作,从而为 Java 平台创建非阻塞 I/O 机制。

Merlin 之前的 I/O 编程

我们将从考察基础的、Merlin 之前的服务器-套接字(server-socket)程序开始。在 ServerSocket 类的生存期中,其重要功能如下:

  • 接受传入连接
  • 从客户机读取请求
  • 为请求提供服务

我们来考察一下以上每一个步骤,我们用代码片段来说明。 首先,我们创建一个新的 ServerSocket :

ServerSocket s = new ServerSocket();

接着,我们要接受传入调用。这里,调用 accept() 应该可以完成任务,但其中有个小陷阱您得当心:

Socket conn = s.accept( );

对 accept() 的调用将一直阻塞,直到服务器套接字接受了一个请求连接的客户机请求。一旦建立了连接,服务器就使用 LineNumberReader读取客户机请求。因为 LineNumberReader 要到缓冲区满时才成批地读取数据,所以这个调用在读时阻塞。 下面的片段显示了工作中的LineNumberReader (阻塞等等)。

InputStream in = conn.getInputStream();
InputStreamReader rdr = new InputStreamReader(in);
LineNumberReader lnr = new LineNumberReader(rdr);
Request req = new Request();
while (!req.isComplete() )
{
   String s = lnr.readLine();
   req.addLine(s);
}

InputStream.read() 是另一种读取数据的方式。不幸的是, read 方法也要一直阻塞到数据可用为止, write 方法也一样,。

图 1 描绘了服务器的典型工作过程。黑体线表示处于阻塞的操作。

图 1. 典型的工作中的服务器

在 JDK 1.4 之前,自由地使用线程是处理阻塞问题最典型的办法。但这个解决办法会产生它自己的问题 ― 即线程开销,线程开销同时影响性能和可伸缩性。不过,随着 Merlin 和 java.nio 包的到来,一切都变了。

在下面的几个部分中,我们将考察 java.nio 的基本思想,然后把我们所学到的一些知识应用于修改前面描述的服务器-套接字示例。

 

回页首

反应器模式(Reactor pattern)

NIO 设计背后的基石是反应器设计模式。 分布式系统中的服务器应用程序必须处理多个向它们发送服务请求的客户机。然而,在调用特定的服务之前,服务器应用程序必须将每个传入请求多路分用并分派到各自相应的服务提供者。反应器模式正好适用于这一功能。它允许事件驱动应用程序将服务请求多路分用并进行分派,然后,这些服务请求被并发地从一个或多个客户机传送到应用程序。

反应器模式的核心功能

  • 将事件多路分用
  • 将事件分派到各自相应的事件处理程序

反应器模式与观察者模式(Observer pattern)在这个方面极为相似:当一个主体发生改变时,所有依属体都得到通知。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联。

请参阅 参考资料了解关于反应器模式的更多信息。

 

回页首

通道和选择器

NIO 的非阻塞 I/O 机制是围绕 选择器和 通道构建的。 Channel 类表示服务器和客户机之间的一种通信机制。与反应器模式一致, Selector 类是 Channel 的多路复用器。 Selector 类将传入客户机请求多路分用并将它们分派到各自的请求处理程序。

我们将仔细考察 Channel 类和 Selector 类的各个功能,以及这两个类如何协同工作,创建非阻塞 I/O 实现。

通道做什么

通道表示连到一个实体(例如:硬件设备、文件、网络套接字或者能执行一个或多个不同 I/O 操作(例如:读或写)的程序组件)的开放连接。可以异步地关闭和中断 NIO 通道。所以,如果一个线程在某条通道的 I/O 操作上阻塞时,那么另一个线程可以将这条通道关闭。类似地,如果一个线程在某条通道的 I/O 操作上阻塞时,那么另一个线程可以中断这个阻塞线程。

图 2. java.nio.channels 的类层次结构

如图 2 所示,在 java.nio.channels 包中有不少通道接口。我们主要关心 java.nio.channels.SocketChannel 接口和java.nio.channels.ServerSocketChannel 接口。 这两个接口可用来分别代替 java.net.Socket 和 java.net.ServerSocket 。尽管我们当然将把注意力放在以非阻塞方式使用通道上,但通道可以以阻塞方式或非阻塞方式使用。

创建一条非阻塞通道

为了实现基础的非阻塞套接字读和写操作,我们要处理两个新类。它们是来自 java.net 包的 InetSocketAddress 类,它指定连接到哪里,以及来自 java.nio.channels 包的 SocketChannel 类,它执行实际的读和写操作。

这部分中的代码片段显示了一种经过修改的、非阻塞的办法来创建基础的服务器-套接字程序。请注意这些代码样本与第一个示例中所用的代码之间的变化,从添加两个新类开始:

String host = ......;
   InetSocketAddress socketAddress = new InetSocketAddress(host, 80);

SocketChannel channel = SocketChannel.open();
   channel.connect(socketAddress);

缓冲区的角色

Buffer 是包含特定基本数据类型数据的抽象类。从本质上说,它是一个包装器,它将带有 getter/setter 方法的固定大小的数组包装起来,这些 getter/setter 方法使得缓冲区的内容可以被访问。 Buffer 类有许多子类,如下:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

ByteBuffer 是唯一支持对其它类型进行读写的类,因为其它类都是特定于类型的。一旦连接上,就可以使用ByteBuffer 对象从通道读数据或将数据写到通道。请参阅 参考资料了解关于 ByteBuffer 的更多信息。

为了使通道成为非阻塞的,我们在通道上调用 configureBlockingMethod(false) ,如下所示:

channel.configureBlockingMethod(false);

在阻塞模式中,线程将在读或写时阻塞,一直到读或写操作彻底完成。如果在读的时候,数据尚未完全到达套接字,则线程将在读操作上阻塞,一直到数据可用。

在非阻塞模式中,线程将读取已经可用的数据(不论多少),然后返回执行其它任务。如果将真(true)传递给 configureBlockingMethod(),则通道的行为将与在 Socket 上进行阻塞读或写时的行为完全相同。唯一的主要差别,如上所述,是这些阻塞读和写可以被其它线程中断。

单靠 Channel 创建非阻塞 I/O 实现是不够的。要实现非阻塞 I/O, Channel 类必须与 Selector 类配合进行工作。

选择器做什么

在反应器模式情形中, Selector 类充当 Reactor 角色。 Selector 对多个 SelectableChannels 的事件进行多路复用。每个 Channel 向Selector 注册事件。当事件从客户机处到来时, Selector 将它们多路分用并将这些事件分派到相应的 Channel 。

创建 Selector 最简单的办法是使用 open() 方法,如下所示:

Selector selector = Selector.open();

通道遇上选择器

每个要为客户机请求提供服务的 Channel 都必须首先创建一个连接。下面的代码创建称为 Server 的 ServerSocketChannel 并将它绑定到本地端口:

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
InetAddress ia = InetAddress.getLocalHost();
InetSocketAddress isa = new InetSocketAddress(ia, port );
serverChannel.socket().bind(isa);

每个要为客户机请求提供服务的 Channel 都必须接着将自己向 Selector 注册。 Channel 应根据它将处理的事件进行注册。例如,接受传入连接的 Channel 应这样注册,如下:

SelectionKey acceptKey =
    channel.register( selector,SelectionKey.OP_ACCEPT);

Channel 向 Selector 的注册用 SelectionKey 对象表示。满足以下三个条件之一, Key 就失效:

  • Channel 被关闭。
  • Selector 被关闭。
  • 通过调用 Key 的 cancel() 方法将 Key 本身取消。

Selector 在 select() 调用时阻塞。接着,它开始等待,直到建立了一个新的连接,或者另一个线程将它唤醒,或者另一个线程将原来的阻塞线程中断。

注册服务器

Server 是那个将自己向 Selector 注册以接受所有传入连接的 ServerSocketChannel ,如下所示:

SelectionKey acceptKey = serverChannel.register(sel, SelectionKey.OP_ACCEPT);
   while (acceptKey.selector().select() > 0 ){
     ......

Server 被注册后,我们根据每个关键字(key)的类型以迭代方式对一组关键字进行处理。一个关键字被处理完成后,就都被从就绪关键字(ready keys)列表中除去,如下所示:

Set readyKeys = sel.selectedKeys();
    Iterator it = readyKeys.iterator();
while (it.hasNext())
{
SelectionKey key = (SelectionKey)it.next();
  it.remove();
  ....
  ....
  ....
 }

如果关键字是可接受(acceptable)的,则接受连接,注册通道,以接受更多的事件(例如:读或写操作)。 如果关键字是可读的(readable)或可写的(writable),则服务器会指示它已经就绪于读写本端数据:

SocketChannel socket;
if (key.isAcceptable()) {
    System.out.println("Acceptable Key");
    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
    socket = (SocketChannel) ssc.accept();
    socket.configureBlocking(false);
    SelectionKey another =
      socket.register(sel,SelectionKey.OP_READ|SelectionKey.OP_WRITE);
}
if (key.isReadable()) {
    System.out.println("Readable Key");
    String ret = readMessage(key);
    if (ret.length() > 0) {
      writeMessage(socket,ret);
    }

}
if (key.isWritable()) {
    System.out.println("Writable Key");
    String ret = readMessage(key);
    socket = (SocketChannel)key.channel();
    if (result.length() > 0 ) {
      writeMessage(socket,ret);
    }
    }
 

回页首

唵嘛呢叭咪吽 — 非阻塞服务器套接字快显灵!

对 JDK 1.4 中的非阻塞 I/O 的介绍的最后一部分留给您:运行这个示例。

在这个简单的非阻塞服务器-套接字示例中,服务器读取发送自客户机的文件名,显示该文件的内容,然后将内容写回到客户机。

这里是您运行这个示例需要做的事情:

  1. 安装 JDK 1.4(请参阅 参考资料)。
  2. 将两个 源代码文件复制到您的目录。
  3. 编译和运行服务器, java NonBlockingServer 。
  4. 编译和运行客户机, java Client 。
  5. 输入类文件所在目录的一个文本文件或 java 文件的名称。
  6. 服务器将读取该文件并将其内容发送到客户机。
  7. 客户机将把从服务器接收到的数据打印出来。(由于所用的 ByteBuffer 的限制,所以将只读取 1024 字节。)
  8. 输入 quit 或 shutdown 命令关闭客户机。
 

回页首

结束语

Merlin 的新 I/O 包覆盖范围很广。Merlin 的新的非阻塞 I/O 实现的主要优点有两方面:线程不再在读或写时阻塞,以及 Selector 能够处理多个连接,从而大幅降低了服务器应用程序开销。

我们已经着重论述了新的 java.nio 包的这两大优点。我们希望,您将把在这里所学到的知识应用到自己的实际应用程序开发工作中。

参考资料

【IBM】Merlin 给 Java 平台带来了非阻塞 I/O的更多相关文章

  1. java网络通信:异步非阻塞I/O (NIO)

    转: java网络通信:异步非阻塞I/O (NIO) 首先是channel,是一个双向的全双工的通道,可同时读写,而输入输出流都是单工的,要么读要么写.Channel分为两大类,分别是用于网络数据的S ...

  2. Java 理论与实践: 非阻塞算法简介——看吧,没有锁定!(转载)

    简介: Java™ 5.0 第一次让使用 Java 语言开发非阻塞算法成为可能,java.util.concurrent 包充分地利用了这个功能.非阻塞算法属于并发算法,它们可以安全地派生它们的线程, ...

  3. Java 理论与实践: 非阻塞算法简介--转载

    在不只一个线程访问一个互斥的变量时,所有线程都必须使用同步,否则就可能会发生一些非常糟糕的事情.Java 语言中主要的同步手段就是synchronized 关键字(也称为内在锁),它强制实行互斥,确保 ...

  4. Java 理论与实践-非阻塞算法简介

    在不只一个线程访问一个互斥的变量时,所有线程都必须使用同步,否则就可能会发生一些非常糟糕的事情.Java 语言中主要的同步手段就是 synchronized 关键字(也称为内在锁),它强制实行互斥,确 ...

  5. Java并发容器之非阻塞队列ConcurrentLinkedQueue

    参考资料:http://blog.csdn.net/chenchaofuck1/article/details/51660521 实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,阻塞队列就是 ...

  6. java网络通信之非阻塞通信

    java中提供的非阻塞类主要包含在java.nio,包括: 1.ServerSocketChannel:ServerSocket替代类,支持阻塞与非阻塞: 2.SocketChannel:Socket ...

  7. JAVA 中BIO,NIO,AIO的理解以及 同步 异步 阻塞 非阻塞

    在高性能的IO体系设计中,有几个名词概念常常会使我们感到迷惑不解.具体如下: 序号 问题 1 什么是同步? 2 什么是异步? 3 什么是阻塞? 4 什么是非阻塞? 5 什么是同步阻塞? 6 什么是同步 ...

  8. 如何解读 Java IO、NIO 中的同步阻塞与同步非阻塞?

    原文链接:如何解读 Java IO.NIO 中的同步阻塞与同步非阻塞? 一.前言 最近刚读完一本书:<Netty.Zookeeper.Redis 并发实战>,个人觉得 Netty 部分是写 ...

  9. 关于同步,异步,阻塞,非阻塞,IOCP/epoll,select/poll,AIO ,NIO ,BIO的总结

    相关资料 IO基本概念 Linux环境 同步异步阻塞非阻塞 同步与异步 阻塞与非阻塞 IO模型Reference Link 阻塞IO模型 非阻塞IO模型 IO复用模型 信号驱动异步IO模型 异步IO模 ...

随机推荐

  1. 0x1L

    整形常量的后缀表示其类型,包括如下后缀,其中U和L的大小写通用 后缀        类型L           long int LL         long long int U          ...

  2. HDU 2993 MAX Average Problem dp斜率优化

    MAX Average Problem Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Othe ...

  3. 支付宝openssl漏洞肆虐 互联网巨头称目前已修复

    支付宝openssl漏洞肆虐 互联网巨头称目前已修复 金山毒霸安全专家李铁军表示,这个漏洞使黑客可以远程读取https服务器的随机64KB内存,“只要这个黑客有耐心多捕获多分析那些64KB的数据,用户 ...

  4. YII model模型和登陆详解

    模型是 CModel 或其子类的实例.模型用于保持数据以及与其相关的业务逻辑. 模型是单独的数据对象.它可以是数据表中的一行,或者一个用户输入的表单. 数据对象的每个字段对应模型中的一个属性.每个属性 ...

  5. 14.6.4 Configuring the Memory Allocator for InnoDB 配置InnoDB 内存分配器

    14.6.4 Configuring the Memory Allocator for InnoDB 配置InnoDB 内存分配器 当InnoDB 被开发时,内存分配提供了操作系统和 run-time ...

  6. bzoj1391

    很像最大权闭合子图的题目s向每个工作连边,流量为收益每个工序,由工作i向对应机器连边,流量为租用费每个机器向t连边,流量为购买费显然跑最小割,ans=总收益-mincut ; type node=re ...

  7. 这几天阅读的shadowgun的几个shader

    直接从阅读时记录的笔记摘抄过来,写的比较随意. 1. MADFINGER-blinking-god-rays 除了可以用于实现太阳光线效果,还能调整参数让颜色随时间淡入淡出闪烁,能做出想灯光之类的效果 ...

  8. POJ 1503 Integer Inquiry 简单大数相加

    Description One of the first users of BIT's new supercomputer was Chip Diller. He extended his explo ...

  9. vijosP1285 佳佳的魔法药水

    vijosP1285 佳佳的魔法药水 链接:https://vijos.org/p/1285 [思路] 图论思想. 很巧妙. 如A+B=C,将AB之间连边,边权为C,用以找相连物品与合成物. 用Dij ...

  10. offsetTop offsetLeft offsetWidth offsetHeight

    document // Html 的容器对象. document.documentElement //html 对象 document.body // body 对象 $(document.docum ...