Java NIO AIO介绍、示例及性能分析

参考:http://tutorials.jenkov.com/java-nio/index.html

1、Java NIO Tutorial

NIO最早出现在Java 1.4版本中,从那个时候开始,Java至少有两套可用的IO方面的API集,一套是标准的,另一套就是NIO,两者的工作原理不同。

Java NIO: Channels and Buffers

在标准IO系统中,用户直接与字节流或者字符流打交道。在NIO中用户总是与channel与buffer打交道,读操作意味着数据从channel读入到buffer,而写操作则意味着将buffer中的数据写入到channel。

Java NIO: Non-blocking IO

NIO能够使IO操作以非阻塞的方式工作。例如,线程可以请求channel读数据到buffer,在channel读数据到buffer的过程中,线程可以去做其它的事情,一旦数据被读入进buffer,线程可以回过头来处理buffer中的数据,将buffer中的数据写入到channel与此过程类似。

Java NIO: Selectors

NIO包含“选择器”的概念,本质上它是一个channel的多路复用器,能够监视注册在其上的channel状态,如连接打开,或者连接上有数据到达,channel处于可读状态,或者内核写缓存有剩余空间,channel处于可写状态等。这样的话,单个线程可以处理多路channel的读写,低层应该是利用操作系统提供的内核函数实现,如Linux中的selector、poll、epoll等。

本节关于NIO的介绍比较笼统,不必深究,容易把自己搞迷糊,只需要知道大概概念就可以,详细的实现原理后边的章节会介绍。

2、Java NIO Channel

Channel是Java NIO的核心概念之一,类似标准IO中的stream,但存在几个不同的点,这些很关键,如下:

  1. Channel可同时读、写,而stream是单向的,要不就只读,要不就只写。
  2. Channel可异步读写(原文:Channels can be read and written asynchronously),我觉得这句话不准确,应该是Channel可非阻塞读写,非阻塞与异步是不同的概念。
  3. Channel总是通过buffer进行读写操作。读的时候将数据从数据源写入buffer,写的时候将数据从buffer写入channel。

注意channel与stream的相同点与不同点,这很重要。

Channel Implementations

Channel是一个抽象的概念,它有如下几种具体的实现:

  • FileChannel,文件。
  • DatagramChannel,UDP。
  • SocketChannel,TCP客户端。
  • ServerSocketChannel,TCP服务端。

Basic Channel Example

下边的代码虽然简单,但基本说明了channel的工作原理,需要仔细解读,我添加了一些注释。

// 打开文件,注意使用的是RandomAccessFile类
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
// 取得FileChannel,这说明了FileChannel是如何创建出来的。
FileChannel inChannel = aFile.getChannel();
// 分配字节类型的缓存,长度是48。显然增长缓存长度可降低读写次数,但过长的话可能浪费空间。
ByteBuffer buf = ByteBuffer.allocate(48);
// 下边这行代码很关键,涉及到channel,channel低层的文件,以及刚刚分配好的buffer。
// 要求channel从文件读数据进来,并且写入到buffer中,buffer最大48个字节。
// 另外read操作不会阻塞,不管是否能读到数据,它都立即返回。
// 返回值代表读到的字节数,如果为-1表示文件结尾,它有可能是0,但不会超过48。
int bytesRead = inChannel.read(buf);
// 文件全部读完后,循环退出。这里体现出非阻塞的好处,不管有没有数据,不管是否有48个字节的数据,读操作立刻返回,线程可以做其它的事情。
while (bytesRead != -1) {
    // 输出读到的字节数
    System.out.println("Read " + bytesRead);
    // 当调用channel的read方法后,buffer处于被channel写入的状态,处于写模式。
    // 调用下边这句话后,buffer就要切换状态到读模式,此时用户可从buffer中取出数据。
    buf.flip();
    // 将buffer中的数据按字节全部取出
    while(buf.hasRemaining()){
        System.out.print((char) buf.get());
    }
    // 清空buffer中的数据
    buf.clear();
    // 继续让channel从文件读数据并写入buffer。
    bytesRead = inChannel.read(buf);
}
// 关闭文件
aFile.close();

3、Java NIO Buffer

Buffer在NIO中是另一个极其重要的概念,非常关键。在NIO模式下,用户主要与channel与buffer打交道,相对来说与buffer打交道的机会更多一些。Channel相当于是一个双向通道,将后端的数据源如文件、socket等与buffer连接起来。一旦通道建立完成,其它时间则主要与buffer打交道,此时可以把buffer看成是数据源的代理或者是用户与数据源之间的中间商,操作buffer就相当于间接操作数据源,并且操作是非阻塞的。

Buffer首先是内存中的一块存储空间,当然它不可能只是一块单纯的内存,为了协调数据源与用户,NIO中的buffer封装了一些特有的成员与方法。

Basic Buffer Usage

使用buffer读写数据,无论是读还是写都涉及四个具体的小步骤,下边分别详细说明。

读操作:

  1. 向buffer写入数据。这一步通过调用channel的read()方法实现,例如本文第二节中出现的代码:bytesRead = inChannel.read(buf);,要求channel从它后端的数据源中read出数据并写入buffer,注意channel的非阻塞特性,此时buffer处于写模式。
  2. 调用buffer的flip()方法,实现写模式到读模式的转换,表示用户接下来将从buffer中读数据。
  3. 用户从buffer中将数据读出,例如调用buffer的get()方法等。
  4. 调用buffer的clear()方法,彻底清空buffer中的数据,既使buffer中存在着用户尚未读出来的数据。或者compact()方法,只清空已经读出来的数据,未读出的数据则继续保留并移动到buffer的开始处。假如我们还需要继续从文件读数据,则继续调用channel的read()方法,并循环以上过程,只到在某个条件满足退出循环。

写操作:

  1. 用户向buffer中写入或者追加数据,此时buffer处于写模式。
  2. 调用buffer的flip()方法,实现写模式到读模式的转换,表示接下来会要求channel将buffer中数据读出并写入到后端的数据源。
  3. 从buffer中读出数据。这一步通过调用channel的write()方法实现,这要求channel从buffer中读出数据并写入到后端的数据源,注意channel的非阻塞特性。
  4. 用户判断buffer的状态,确认写入的进度,如buffer中的数据有多少已经被channel写入到后端,有多少尚未写入。用户可持续这四个步骤,直到写入全部数据。

读操作的简单代码可参考第二节。

Buffer Capacity, Position and Limit

要理解buffer如何工作,需要熟悉它的三个属性。

  • capacity
  • position
  • limit

Capacity表示buffer的容量,容量在创建buffer时指定,它是一成不变的。通过前边的介绍可知,buffer有模式,通过flip()方法可在读、写模式之间切换。模式不同,则position与limit的含义也不同。先看一张图,稍后解释它。

                                      Java NIO AIO介绍、示例及性能分析_第1张图片

看一下上图中右边的图,它表示写模式下的buffer。此时,postion代表下一个可以写入的位置,最开始时这个值是0,随着数据的增加position会持续变大。而limit此时与capacity相同,表示position的最大值限制,postion不能超过limit的限制。

将buffer由写模式切换到读模式后,postion与limit的含义及值均发生变化。postion指向buffer开始的位置,也就是0。而limit则指向写模式下的postion的位置。随着读操作的进行,postion的值增加,但它一定小于limit的值,因为limit及其以后的空间是无效的,还没有写入数据。

假如再将buffer由读模式切换到写模式,有三种可能。

  1. 如果在读模式下buffer中的数据即没有通过clear()方法彻底清空,也没有通过compact()方法将已经读取的数据清空,则buffer切换回写模式后什么都不会变,相当于没有切换。
  2. 如果在读模式下buffer中的数据通过clear()方法彻底清空,则切换回写模式后,postion将指向0的位置,相当于这是一个空的buffer。
  3. 如果在读模式下buffer中的数据通过调用compact()方法,只是将已经读取过的数据清空。则切换回写模式后,buffer中剩余的未读数据将会向buffer的顶端移动,同时postion的位置也会向顶端方向移动,也就是未读数据仍然保留。

Buffer Types

Buffer的具体实现有几下几种:

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

Allocating a Buffer

ByteBuffer buf = ByteBuffer.allocate(48);
CharBuffer buf = CharBuffer.allocate(1024);

Writing Data to a Buffer

在读操作时,由channel将后端数据写入到buffer,在写操作时,由用户将数据写入到buffer。

读操作下channel向buffer写入数据:

int bytesRead = inChannel.read(buf); //read into buffer.

这里向buffer中写入数据,但channel的方法却是read(),有点奇怪。对于channel的read()方法而言,更细致的说法是read then write,先从数据源读,然后再写入buffer。

用户向buffer与入数据:

buf.put(127);  

buffer的put()方法有很多版本,用户可以以多种方式向buffer中写入数据,如果需要可查询Java提供的随机文档。

flip()方法

调用此方法后,如果buffer处于写模式则切换到读模式,反之亦然,它内部的实现原理在介绍buffer如何工作时已经说明。

Reading Data from a Buffer

写操作下channel从buffer读数据:

//read from buffer into channel.
int bytesWritten = inChannel.write(buf);

与上边的写数据到buffer一样,这句代码的意思是让channel从buffer中读出数据并写入到后端。

用户从buffer读数据:

byte aByte = buf.get(); 

get()方法也有很多实现版本,可查阅随机文档。

remaining()

此方法返回一个整数,其值是limit-postion的值。在写模式下,表示buffer可以写入的剩余条目数量。在读模式下,表示buffer中可以读出的剩余条目数量。

rewind()

在读模式下将postion的位置归0,以实现重复读的目的。

clear() and compact()

在读模式下通过这两个方法清除buffer中的数据,便于重复利用buffer。clear()方法会彻底清空buffer,不管buffer中是否存在未读数据,如果有未读数据,调用此方法后,将不会再有机会读取它们。compact()方法只清楚已经读出来的数据,没有读过的数据仍然保留。

mark() and reset()

这是一对方法。在读模式下mark()方法可以将当前的postion记住,然后postion向前移动。当调用reset()方法后,postion会恢复到原来调用mark()方法时记录下来的位置。

示例代码:

buffer.mark();

//call buffer.get() a couple of times, e.g. during parsing.

buffer.reset();  //set position back to mark. 

equals() 

如果满足以下三个条件,则两个buffer相等:

  1. 类型相同
  2. 剩余的元素个数相同(remaining方法返回的值相同)
  3. 剩余的元素逐个对比,每个元素都相同

需要注意,两个buffer是否相等,取决于buffer中的剩余元素而非整个buffer的全部元素,与buffer中的limit与postion具体的值、capacity的值都没有关系。

compareTo()

比较两个buffer的大小,参与比较的仍然是两个buffer中的remaining元素。如果两个buffer中remaining元素的个数不同,则以元素较少的那个为准,例如当前buffer有10个元素,对方buffer有20个元素,则参与对比的元素是10个。

先从头开始逐个比较这10个元素,如果在10个元素中有任何一个不相等则比较结束,谁的元素大谁就大,谁的元素小谁就小。

如果这10个元素全部对比完成并且全部相等,则emaining元素更多的那个被认为更大,也就是对方buffer更大,因为它有20个元素。

如果对方buffer也有10个元素,并且这10个元素完全相等,则两个buffer被认为相等。

4、Java NIO Selector

Channel与Buffer解决的是非阻塞的问题,单个线程以非阻塞的方式处理单个IO。而Selector是channel的多路复用器。通过一个Selector实例,单个线程可以同时监控多个channel的状态,如可读、可写等,进而实现单个线程处理多路IO,以提高线程的利用效率,相对来说Selector的概念要复杂一些。以下为示意图:

                                             Java NIO AIO介绍、示例及性能分析_第2张图片

Creating a Selector

Selector selector = Selector.open();

Registering Channels with the Selector

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

上面的代码有三点需要说明一下。

第一点:首先第一行代码,设置channel为非阻塞模式,注册到Selector的channel必需工作在非阻塞模式,因此FileChannel无法与Selector配合工作,因为它不能工作在非阻塞模式下,SocketChannel可以。

第二点:第二行,channel是通过调用自己的register()方法将自己注册到某个Selector,注意不是Selector调用自己的方法将某个channel加入进来,可以想见,channel解除与Selector的关系时也由调用自己的方法实现。另外看register()方法的第二个参数,表示channel想让Selector监控的操作类型,称为“兴趣集”,有以下四种:

  1. SelectionKey.OP_CONNECT:表示客户端socket已经与服务端建立连接。
  2. SelectionKey.OP_ACCEPT:表示服务端socket收到客户端的连接建立请求,将与对方建立了连接。
  3. SelectionKey.OP_READ:表示有数据到达内核中的读缓存,用户可以读数据。
  4. SelectionKey.OP_WRITE:表示内核中的写缓存有空间,允许用户写入数据。

兴趣集可以是以上四个中的多个,如:

SelectionKey.OP_READ | SelectionKey.OP_WRITE

第三点:channel的register()方法会返回一个SelectionKey对象,SelectionKey非常关键,Selector内部通过这个类的实例管理注册到其上的channel,后续的大部份的操作都围绕着SelectionKey展开,比如后边讲到的select()方法,它返回的是SelectionKey的一个集合,后边会详细说明。

SelectionKey

那么SelectionKey内部都包含什么呢?如下:

  • The interest set,兴趣集,在register()方法中确定
  • The ready set,就绪的兴趣集,兴趣集可以包含多个需要监控的操作,这个集合表示有那些操作处于就绪状态。
  • The Channel,这个SelectionKey负责管理的channel。
  • The Selector,当前的SelectionKey所管理的channel由那个Selector负责监控。
  • An attached object (optional),可选的附件object。可选,当channel向Selector注册时,可以追加一个对象作为附件,如:
    CharBuffer writeBuffer = CharBuffer.wrap("hello nio");
    channel.register(selector, SelectionKey.OP_WRITE, writeBuffer);

    这样writeBuffer就会成为attached object,当用户通过Selector的select()方法发现此channel写就绪时,可通过返回的SelectionKey将此buffer取出,然后直接将其中的数据写入到channel中。

判断Interest Set

可通过如下方法,判断SelectionKey中的兴趣集:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;    

判断Ready Set

可通过如下方法判断其内容:

int readySet = selectionKey.readyOps();

然后通过与上边一样的&操作判断其具体内容。

下边是更简单直接的方法:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

取得Channel + Selector

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector(); 

添加并取得Attaching Objects

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector(); 

或者在注册时就加入附件,如:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

Selecting Channels via a Selector

将channel注册到Selector后,可通过select()方法返回处于就绪状态的Channel的数量,select()方法有三个版本:

  1. int select(),阻塞,一直等到有channel处于就绪状态。
  2. int select(long timeout),阻塞指定的毫秒数,如果没有channel处于就绪状态,到期后返回。
  3. int selectNow(),不阻塞立刻返回。

当然以上说得是常规情况,处于阻塞状态的select有可能会被中断、强制被其它线程唤醒等,随机文档有详细说明。

selectedKeys()

select()方法返回的是处于就绪状态的channel的个数,如果此方法返回值大于0,说明有channel就绪,此时可以通过selectedKeys()方法返回处于就绪状态的channel。当然Selector通过SelectionKey管理注册到里边的channel,因此返回结果是一个SelectionKey的集合,例如:

Set selectedKeys = selector.selectedKeys();  

遍历集合:

Set selectedKeys = selector.selectedKeys();

Iterator keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

注意最后的remove()方法,用户处理完集合中的某个SelectionKey后,应该通过迭代器将它从集合中删除,否则下次调用selectedKeys()方法它仍然还会返回,从而形成死循环。

wakeUp()

唤醒操作。如果一个线程调用select()方法阻塞不,另一个线程调用同一个Selector的方法,可使前一个线程的select()方法立刻返回。

close()

关闭Selector,但是不关闭注册到里边的channel。

完整示例

Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);


while(true) {

  int readyChannels = selector.selectNow();

  if(readyChannels == 0) continue;


  Set selectedKeys = selector.selectedKeys();

  Iterator keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}

Dig Deep Selector

通过以上的介绍可知,Selector通过SelectionKey管理注册到里边的channel,在Selector内部有三个SelectionKey类型的集合:

  • registed-set:包含注册进Selector的全部channel,可通过keys()方法返回。
  • selected-set:包含处于就绪状态的channel集合,可通过selectedKeys()方法返回。
  • cancelled-set:这个集合里边的channel指的是Selector不需要再监控channel,但channel还没有来得及解除与Selector的注册关系。此集合为Selector内部使用,没有方法可以返回。

在初始状态,以上三个集合都为空。

Channel通过调用自己的register()方法将自己注册进某个Selector也就是加入到registed-set中,当调用channel的cancel()或者close()方法时,则将自己加入到cancelled-set中。当调用select()方法时,cancelled-set中的channel会时同从registed-set与selected-set中清除,这样此channel与Selector就彻底没有关系了。

对于selected-set中的内容,由select()方法添加进去,用户可通过selectedKeys()返回此集合。需要注意的是,当处理完成后,此集合中的条目只能由用户通过集合的remove()方法或者迭代器的remove()方法删除,Selector内部只负责向这个集合添加,绝对不管删除。如果用户没有删除此集合中处理完成的条目,那么应该明确此行为引起的副作用,下边会说到。

select()方法执行的三个步骤:

  1. 清空cancelled-set,时同将cancelled-set中的channel从registed-set与selected-set中删除,channle彻底解除注册。

  2. 如果registed-set中虽然有channel,但是所有的channel都没有设置兴趣集,则立刻返回,并且selected-set不会被更新。否则,查询低层操作系统,查看registed-set中的channel是否处于兴趣集中的就绪状态,如果有,则执行如下的两个动作中的一个:

    1. 如果在selected-set此channel不存在,则将管理channel的SelectionKey加入到elected-set集合中,其中旧的ready-operation标志位将被重置,用新的ready-operation代替,以指明到底是什么操作就绪了。

    2. 否则如果在selected-key此channel存在,则修改其ready-operation标志位,旧的标志不会被重置而是保留,这个就是用户没有删除selected-key集合中条目的副作用,就绪的channel会再次返回。

  3. 如果在第二步处理的过程当中,有channel加入到cancelled-set集合中,则按步骤1时清除。

关于Selector的并发问题

在多线程并发的情况下,Selector本身是线程安全的,然则它内部管理的selectionKey以及集合则不是。

如果在selector()方法执行期间,其它线程修改SelectionKey的兴趣集,对正在执行的selector()方法不会产生影响,修改会在下次执行selector()方法时生效。

Selector集合中SelectionKey管理的channel,其有效状态随时随地都可能变化。因此Selector的集合中存在一个SelectionKey,并不代表相应的channel是有效的或者是打开的,有可能被取消、被对端或者其它线程关闭,Selector只是负责监控而已。因此在多线程情况下,应用程序代码应该仔细同步多线程对SelectionKey的操作。

对于registed-set与selected-set,用户可以通过方法返回它们。但是在多线程环境下操作这两个集合不是线程完全的,需要特别提供同步措施。同时,这两个集合返回的iterator是fail-fast的,如果在遍历期间集合发生变化,则会抛出

ConcurrentModificationException异常。

总结一下同步问题,总之registed-set、selected-set、cancelled-set这三个集合只有一份。Selector本身操作它们时保证线程安全。但是如果用户通过方法返回它们后在多线程环境下操作它们及其中的元素,显然是不安全的,需要自行采取同步措施。

5、NIO SocketChannel+Selector示例

本示例向tomcat发送get请求,可设置单个线程同时监控连接的数量,可设置发送get请求的最大值。

package com.zhangdb.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

class HttpConnector {

	protected final String host;
	protected final int port;
	protected final Selector selector;
	protected boolean verbose;
	protected final long startTime;

	public HttpConnector(String host, int port, Selector selector, boolean verbose) {
		this.host = host;
		this.port = port;
		this.selector = selector;
		this.verbose = verbose;
		this.startTime = System.currentTimeMillis();
	}

	public void connect() throws IOException {

		SocketChannel channel = SocketChannel.open();
		channel.configureBlocking(false);

		channel.register(selector, SelectionKey.OP_CONNECT, this);

		if (verbose)
			System.out.println(channel.hashCode() + "\t" + "Connecting......");

		channel.connect(new InetSocketAddress(host, port));
	}

	public void finish(SelectionKey key) throws IOException {

		SocketChannel channel = (SocketChannel) key.channel();
		channel.finishConnect();

		if (verbose)
			System.out.println(channel.hashCode() + "\t" + "Connect finished");
	}
}

class HttpRequest {

	protected final String path;
	protected volatile ByteBuffer buffer;
	protected final boolean verbose;
	protected long startTime;

	public HttpRequest(String path, boolean verbose) {
		this.path = path;
		buffer = null;
		this.verbose = verbose;
	}

	public void send(SelectionKey key) throws IOException {

		HttpConnector connector = (HttpConnector) key.attachment();

		this.startTime = connector.startTime;

		buffer = ByteBuffer.wrap(("GET " + path + " HTTP/1.1\r\n" + "Host: " + connector.host + ":" + connector.port
				+ "\r\nAccept: text/html\r\nConnection: closed\r\n" + "\r\n\r\n").getBytes());

		key.channel().register(key.selector(), SelectionKey.OP_WRITE, this);
	}

	public boolean isFinished(SelectionKey key) throws IOException {

		boolean flag = false;

		SocketChannel channel = (SocketChannel) key.channel();
		if (verbose)
			System.out.println(channel.hashCode() + "\t" + "Request sending......");

		if (buffer.hasRemaining()) {
			channel.write(buffer);
		} else {
			if (verbose)
				System.out.println(channel.hashCode() + "\t" + "Request send finished");

			flag = true;
		}

		return flag;
	}
}

class HttpResponse {

	protected final ByteBuffer buffer;
	protected final boolean verbose;
	protected long startTime;

	public HttpResponse(boolean verbose) {
		buffer = ByteBuffer.allocate(1024 * 20);
		this.verbose = verbose;
	}

	public void receive(SelectionKey key) throws IOException {

		HttpRequest request = (HttpRequest) key.attachment();

		this.startTime = request.startTime;

		key.channel().register(key.selector(), SelectionKey.OP_READ, this);
	}

	public boolean isFinished(SelectionKey key, boolean displayResponse) throws IOException, InterruptedException {

		boolean flag = false;

		SocketChannel channel = (SocketChannel) key.channel();
		int byteSize = channel.read(buffer);
		if (byteSize == -1) {
			channel.close();

			NioDemo.statistic.addAndGet(System.currentTimeMillis() - startTime);

			if (verbose)
				System.out.println(channel.hashCode() + "\t" + "Response receive finished");

			flag = true;
		} else if (byteSize > 0) {
			if (verbose)
				System.out.println(channel.hashCode() + "\t" + "Response receiving......" + byteSize);

			if (displayResponse) {
				buffer.flip();
				byte[] temp = new byte[byteSize];
				buffer.get(temp, 0, byteSize);
				System.out.println(new String(temp));
			}

			buffer.clear();
		}
		return flag;
	}
}

public class NioDemo {
	public static final AtomicLong statistic = new AtomicLong();

	private static void SelectorListenLoop(Selector selector, int loopTimes, int concurrentSize)
			throws IOException, InterruptedException {

		long start = System.currentTimeMillis();
		
		int counter = loopTimes;

		for (int i = 0; i < concurrentSize; i++) {
			new HttpConnector("localhost", 8080, selector, false).connect();
		}

		boolean loopFlag = true;
		while (loopFlag) {
			int selectedCount = selector.select();
			if (selectedCount == 0) {
				continue;
			}

			Set keys = selector.selectedKeys();
			Iterator keyIterator = keys.iterator();

			while (keyIterator.hasNext()) {
				SelectionKey key = keyIterator.next();

				if (key.isAcceptable()) {
				} else if (key.isConnectable()) {
					HttpConnector connector = (HttpConnector) key.attachment();
					connector.finish(key);
					new HttpRequest("/docs/manager-howto.html", false).send(key);
				} else if (key.isReadable()) {
					HttpResponse response = (HttpResponse) key.attachment();
					if (response.isFinished(key, false)) {
						if (counter % 100 == 0) {
							System.out.println("==========" + counter + "==========");
						}
						if (counter-- > concurrentSize) {
							new HttpConnector("localhost", 8080, selector, false).connect();
						} else {
							loopFlag = false;
						}
					}
				} else if (key.isWritable()) {
					HttpRequest request = (HttpRequest) key.attachment();
					if (request.isFinished(key)) {
						new HttpResponse(false).receive(key);
					}
				}
				keyIterator.remove();
			}
		}
		System.out.println("##########TOTAL:" + (System.currentTimeMillis() - start) + "##########AVG:"
				+ NioDemo.statistic.get() / loopTimes);
	}

	public static void main(String[] args) throws IOException, InterruptedException {

		Selector selector = Selector.open();

		// 单个线程同时监控多个连接,总共发起20000次连接请求
		SelectorListenLoop(selector, 20000, 256);

		selector.close();
	}
}

做个性能测试,请求comcat中的/docs/manager-howto.html帮助页面,总共20000次,采用短连接,每次tomcat关闭连接算一次请求完成。将单个一程同时监控IO的路数从1开始每次乘2逐步升高,看一下完成20000次请求的总时间及系统CPU、IO的变化情况。

并发路数 总时间(ms) 平均时间(ms) CPU(%) IO(%)
1 61949 3 60 40
2 39478 3 60 70
4 35483 7 60 70
8 34198 13 60 70
16 34988 27 60 70
32 33604 53 60 70
64 32822 104 62 70
128 32695 208 62 70
256 33295 423 67 70
512 36124 911 70 70
1024 37320 1850 70 70

从测试结果可以看出,本应用的性能瓶颈在IO,当并发路数是2时,IO达到70%的峰值,以后再增加并发路数,此值也不会再升高。

从并发路数2开始,每增加一倍,单个请求的处理时间也基本上延长2倍,而CPU的使用量只有微小的变化。

当并发路数是256时,性能最佳,单次请求的平均处理时间为423ms,在大部分系统中都是可以授受的。

当大于256时,因为并发的路数太多,这个时候CPU占用率的上升速度加快,并且单个请求的处理时候很快就会超过1000ms。

因此可以看出,在IO密集型应用中,NIO可以达到非常好的效果,单个线程就可以使用系统的吞吐量达到峰值,实现简单。如果允许的并发多,那么单个请求的处理时间就会长,如果并发路数小,单个请求的处理时间就短一些。

6、Java AIO

从Java 1.7开始,又增加了一种IO模式,就是AIO,至此Java总共有三种IO模式,BIO、NIO、AIO。

NIO实现的是同步非阻塞模式,线程向系统发起IO操作,能写多少就写多少,能读多少就读多少,无论如何都立刻返回,不会发生阻塞。然后对操作返回的结果进行判断,如果操作还没有完成,就等IO下次就绪后接着操作直到完成。在等IO就绪的时间段,线程也以执行其它任务,线程也可以通过Selector监控多路IO的就绪状态,就像上边的例子所演示的一样。

AIO是真正的异步模式,线程向系统发起异步IO操作,整个操作的完成都由系统负责。线程可以等待异步IO的完成,然后再执行后续的操作,这个称为“将来式”。另一种是线程发起异步IO时,同时指明当IO异步操作完成时的后续处理,这个是“回调式”。“回调式”需要线程池的配置,当系统完成异步IO后,将线程指定的后续处理当成一个任务提交给线程池,线程池中的线程空闲时提取任务,执行其中的处理。

与AIO相关的新增加的三个Channel:

  • AsynchronousFileChannel: 文件异步读写
  • AsynchronousSocketChannel: Socket客户端异步
  • AsynchronousServerSocketChannel: Socket服务端异步

在使用AIO时,关键就是在发起异常IO请求如read、write时,要指定一个后续处理的handler实例,这个handler要实现特定的接口。

7、AIO示例

将上边用NIO实现的例子用AIO再实现一次,两段代码实现的功能完全一样,代码如下:

package com.zhangdb.aio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

class HttpConnector implements CompletionHandler {

	protected final String host;
	protected final int port;
	protected final String path;
	protected final boolean verbose;
	protected volatile AsynchronousSocketChannel channel = null;
	protected volatile long startTime;

	public HttpConnector(String host, int port, String path, boolean verbose) {
		this.host = host;
		this.port = port;
		this.path = path;
		this.verbose = verbose;
	}

	public void connect(AsynchronousChannelGroup channelGroup) throws IOException, InterruptedException {
		this.startTime = System.currentTimeMillis();

		AioDemo.concurrentLimit.acquire();
		
		channel = AsynchronousSocketChannel.open(channelGroup);

		if (verbose)
			System.out.println(channel.hashCode() + "\t" + "Connecting......");

		channel.connect(new InetSocketAddress(host, port), this, this);
	}

	@Override
	public void completed(Void result, HttpConnector attachment) {
		if (verbose)
			System.out.println(channel.hashCode() + "\t" + "Connect Finished");

		new HttpRequest(verbose).send(attachment);
	}

	@Override
	public void failed(Throwable exc, HttpConnector attachment) {
		System.out.println(channel.hashCode() + "\t" + "Connect Failed:" + exc.getClass());
		
		AioDemo.concurrentLimit.release();
	}
}

class HttpRequest implements CompletionHandler {

	protected volatile ByteBuffer buffer;
	protected volatile AsynchronousSocketChannel channel;
	protected volatile long startTime;
	protected final boolean verbose;

	public HttpRequest(boolean verbose) {
		buffer = null;
		channel = null;
		this.verbose = verbose;
	}

	public void send(HttpConnector connector) {

		this.startTime = connector.startTime;
		this.channel = connector.channel;

		if (verbose)
			System.out.println(channel.hashCode() + "\t" + "Request Sending......");

		buffer = ByteBuffer.wrap(("GET " + connector.path + " HTTP/1.1\r\n" + "Host: " + connector.host + ":"
				+ connector.port + "\r\nAccept: text/html\r\nConnection: closed\r\n" + "\r\n\r\n").getBytes());

		channel.write(buffer, this, this);
	}

	@Override
	public void completed(Integer result, HttpRequest attachment) {

		if (verbose)
			System.out.println(channel.hashCode() + "\t" + "Request Send Finished");

		new HttpResponse(verbose).receive(attachment);
	}

	@Override
	public void failed(Throwable exc, HttpRequest attachment) {
		System.out.println(channel.hashCode() + "\t" + "Request Send Failed:" + exc.getClass());
		
		AioDemo.concurrentLimit.release();
	}
}

class HttpResponse implements CompletionHandler {

	protected final ByteBuffer buffer;
	protected volatile AsynchronousSocketChannel channel;
	protected final boolean verbose;
	protected long startTime;

	private static final int BUFFER_SIZE = 1024 * 20;

	public HttpResponse(boolean verbose) {
		buffer = ByteBuffer.allocate(BUFFER_SIZE);
		this.verbose = verbose;
	}

	public void receive(HttpRequest request) {

		this.startTime = request.startTime;
		this.channel = request.channel;

		if (verbose)
			System.out.println(channel.hashCode() + "\t" + "Response Receiving......");

		channel.read(buffer, this, this);
	}

	@Override
	public void completed(Integer result, HttpResponse attachment) {

		if (result.intValue() != -1) {

			if (verbose) {
				buffer.flip();
				byte[] temp = new byte[buffer.limit()];
				buffer.get(temp, 0, buffer.limit());
				System.out.println(new String(temp));
			}

			buffer.clear();
			channel.read(buffer, this, this);

		} else {
			if (verbose)
				System.out.println(channel.hashCode() + "\t" + "Response Receive Finished");

			try {
				channel.close();
			} catch (IOException e) {
				e.printStackTrace();
			}

			AioDemo.STATISTICS.addAndGet(System.currentTimeMillis() - startTime);
			
			AioDemo.concurrentLimit.release();
		}
	}

	@Override
	public void failed(Throwable exc, HttpResponse attachment) {
		System.out.println(channel.hashCode() + "\t" + "Response Receive Failed:" + exc.getClass());
		
		AioDemo.concurrentLimit.release();
	}
}

public class AioDemo {

	public static final AtomicLong STATISTICS = new AtomicLong();
	
	// 通过semaphore控制连接的并发数量
	public static Semaphore concurrentLimit = new Semaphore(1024, true);

	public static void main(String[] args) throws IOException, InterruptedException {
		int loopTimes = 20000;

		// 创建线程池
		ExecutorService executor = Executors.newFixedThreadPool(1);

		// 创建异步channel组
		AsynchronousChannelGroup group = AsynchronousChannelGroup.withThreadPool(executor);

		long start = System.currentTimeMillis();
		
		for(int i = 0; i < loopTimes; i ++)
		{
			if (i % 100 == 0) {
				System.out.println("==========" + i + "==========");
			}
			new HttpConnector("localhost", 8080, "/docs/manager-howto.html", false).connect(group);
		}

		// 关闭异步channel组,执行此方法后,异步channel组中不可以再发起新的连接
		group.shutdown();

		// 等待异步channel组中的异步IO全部完成
		group.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);

		System.out.println("==========TOTAL:" + (System.currentTimeMillis() - start) + "AVG:"
				+ STATISTICS.get() / loopTimes + "==========");

		// 关闭线程池,执行此方法后,不可以再向线程池提交新的任务
		executor.shutdown();

		// 等待线程池中所有任务执行完成
		executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
	}
}

做一下性能测试,将线程池的个数设定为1,其它与NIO的测试完全一样,逐步升高连接的并发数,看一下性能的变化,结果如下:

并发路数 总时间(ms) 平均时间(ms) CPU(%) IO(%)
1

61960

60 40
2 53722 7 61 50
4

32705

7 62 80
8 36338 15 63 70
16 33132 27 64 70
32 31345 50 65 80
64 30074 96 66 80
128 29390 186 67 85
256 30916 392 68 90
512 32240 811 79 85
1024 34690 1733 70 80

AIO与NIO,两者在性能方面有微小的差异,这个是由系统扰动引起的,NIO是昨天测试的,AIO是今天测试的。如果在同一时间段测试,两者的表现可以说是一模一样。

AIO应该是NIO的一次进化,它比NIO要更加灵活,代码看起来也更加简洁。在IO密集型的场景下,将线程池的个数设置的小一点比如说1,它效果NIO一样,对于计算密集型的场景,可以高线程池适当调大一点,比如设置成与CPU核心数一致,这样就可以最大限度的利用计算资源。 

你可能感兴趣的:(Java语言)