本文将介绍非阻塞 IO 和异步 IO,也就是大家耳熟能详的 NIO 和 AIO。很多初学者可能分不清楚异步和非阻塞的区别,只是在各种场合能听到异步非阻塞这个词。
本文会先介绍并演示阻塞模式,然后引入非阻塞模式来对阻塞模式进行优化,最后再介绍 JDK7 引入的异步 IO,由于网上关于异步 IO 的介绍相对较少,所以这部分内容我会介绍得具体一些。
希望看完本文,读者可以对非阻塞 IO 和异步 IO 的迷雾看得更清晰些,或者为初学者解开一丝丝疑惑也是好的。
阻塞模式 IO
我们已经介绍过使用 Java NIO 包组成一个简单的客户端-服务端网络通讯所需要的 ServerSocketChannel、SocketChannel 和 Buffer,我们这里整合一下它们,给出一个完整的可运行的例子:
public classServer{ publicstaticvoidmain(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 监听 8080 端口进来的 TCP 链接 serverSocketChannel.socket().bind(new InetSocketAddress(8080)); while (true) { // 这里会阻塞,直到有一个请求的连接进来 SocketChannel socketChannel = serverSocketChannel.accept(); // 开启一个新的线程来处理这个请求,然后在 while 循环中继续监听 8080 端口 SocketHandler handler = new SocketHandler(socketChannel); new Thread(handler).start(); } } }
这里看一下新的线程需要做什么,SocketHandler:
public classSocketHandlerimplementsRunnable{ private SocketChannel socketChannel; publicSocketHandler(SocketChannel socketChannel) { this.socketChannel = socketChannel; } @Override publicvoidrun() { ByteBuffer buffer = ByteBuffer.allocate(1024); try { // 将请求数据读入 Buffer 中 int num; while ((num = socketChannel.read(buffer)) > 0) { // 读取 Buffer 内容之前先 flip 一下 buffer.flip(); // 提取 Buffer 中的数据 byte[] bytes = new byte[num]; buffer.get(bytes); String re = new String(bytes, "UTF-8"); System.out.println("收到请求:" + re); // 回应客户端 ByteBuffer writeBuffer = ByteBuffer.wrap(("我已经收到你的请求,你的请求内容是:" + re).getBytes()); socketChannel.write(writeBuffer); buffer.clear(); } } catch (IOException e) { IOUtils.closeQuietly(socketChannel); } } }
最后,贴一下客户端 SocketChannel 的使用,客户端比较简单:
public classSocketChannelTest{ publicstaticvoidmain(String[] args) throws IOException { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("localhost", 8080)); // 发送请求 ByteBuffer buffer = ByteBuffer.wrap("1234567890".getBytes()); socketChannel.write(buffer); // 读取响应 ByteBuffer readBuffer = ByteBuffer.allocate(1024); int num; if ((num = socketChannel.read(readBuffer)) > 0) { readBuffer.flip(); byte[] re = new byte[num]; readBuffer.get(re); String result = new String(re, "UTF-8"); System.out.println("返回值: " + result); } } }
上面介绍的阻塞模式的代码应该很好理解:来一个新的连接,我们就新开一个线程来处理这个连接,之后的操作全部由那个线程来完成。
那么,这个模式下的性能瓶颈在哪里呢?
-
首先,每次来一个连接都开一个新的线程这肯定是不合适的。当活跃连接数在几十几百的时候当然是可以这样做的,但如果活跃连接数是几万几十万的时候,这么多线程明显就不行了。每个线程都需要一部分内存,内存会被迅速消耗,同时,线程切换的开销非常大。
-
其次,阻塞操作在这里也是一个问题。首先,accept() 是一个阻塞操作,当 accept() 返回的时候,代表有一个连接可以使用了,我们这里是马上就新建线程来处理这个 SocketChannel 了,但是,但是这里不代表对方就将数据传输过来了。所以,SocketChannel#read 方法将阻塞,等待数据,明显这个等待是不值得的。同理,write 方法也需要等待通道可写才能执行写入操作,这边的阻塞等待也是不值得的。
非阻塞 IO
前面说了那么多实现,但是没有出现 Windows,Windows 平台的非阻塞 IO 使用 select,我们也不必觉得 Windows 很落后,在 Windows 中 IOCP 提供的异步 IO 是比较强大的。
public classSelectorServer{ publicstaticvoidmain(String[] args) throws IOException { Selector selector = Selector.open(); ServerSocketChannel server = ServerSocketChannel.open(); server.socket().bind(new InetSocketAddress(8080)); // 将其注册到 Selector 中,监听 OP_ACCEPT 事件 server.configureBlocking(false); server.register(selector, SelectionKey.OP_ACCEPT); while (true) { int readyChannels = selector.select(); if (readyChannels == 0) { continue; } Set<SelectionKey> readyKeys = selector.selectedKeys(); // 遍历 Iterator<SelectionKey> iterator = readyKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (key.isAcceptable()) { // 有已经接受的新的到服务端的连接 SocketChannel socketChannel = server.accept(); // 有新的连接并不代表这个通道就有数据, // 这里将这个新的 SocketChannel 注册到 Selector,监听 OP_READ 事件,等待数据 socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { // 有数据可读 // 上面一个 if 分支中注册了监听 OP_READ 事件的 SocketChannel SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(1024); int num = socketChannel.read(readBuffer); if (num > 0) { // 处理进来的数据... System.out.println("收到数据:" + new String(readBuffer.array()).trim()); ByteBuffer buffer = ByteBuffer.wrap("返回给客户端的数据...".getBytes()); socketChannel.write(buffer); } else if (num == -1) { // -1 代表连接已经关闭 socketChannel.close(); } } } } } }
NIO.2 异步 IO
在 Linux 中其实也是有异步 IO 系统实现的,但是限制比较多,性能也一般,所以 JDK 采用了自建线程池的方式。
1、返回 Future 实例
-
future.isDone();判断操作是否已经完成,包括了正常完成、异常抛出、取消
-
future.cancel(true);取消操作,方式是中断。参数 true 说的是,即使这个任务正在执行,也会进行中断。
-
future.isCancelled();是否被取消,只有在任务正常结束之前被取消,这个方法才会返回 true
-
future.get();这是我们的老朋友,获取执行结果,阻塞。
-
future.get(10, TimeUnit.SECONDS);如果上面的 get() 方法的阻塞你不满意,那就设置个超时时间。
2、提供 CompletionHandler 回调函数
public interfaceCompletionHandler<V,A> { voidcompleted(V result, A attachment); voidfailed(Throwable exc, A attachment); }
注意,参数上有个 attachment,虽然不常用,我们可以在各个支持的方法中传递这个参数值
AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open().bind(null); // accept 方法的第一个参数可以传递 attachment listener.accept(attachment, new CompletionHandler<AsynchronousSocketChannel, Object>() { publicvoidcompleted( AsynchronousSocketChannel client, Object attachment) { // } publicvoidfailed(Throwable exc, Object attachment) { // } });
AsynchronousFileChannel
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("/Users/hongjie/test.txt"));
ByteBuffer buffer = ByteBuffer.allocate(1024); Future<Integer> result = channel.read(buffer, 0);
异步文件通道的读操作和写操作都需要提供一个文件的开始位置,文件开始位置为 0
public abstract <A> voidread(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer,? super A> handler);
publicabstract Future<Integer> write(ByteBuffer src, long position); public abstract <A> voidwrite(ByteBuffer src, long position, A attachment, CompletionHandler<Integer,? super A> handler);
publicabstractvoidforce(boolean metaData) throws IOException;
因为我们对文件的写操作,操作系统并不会直接针对文件操作,系统会缓存,然后周期性地刷入到磁盘。如果希望将数据及时写入到磁盘中,以免断电引发部分数据丢失,可以调用此方法。参数如果设置为 true,意味着同时也将文件属性信息更新到磁盘。
publicabstract Future<FileLock> lock(long position, long size, boolean shared);
position 是要锁定内容的开始位置,size 指示了要锁定的区域大小,shared 指示需要的是共享锁还是排他锁
public abstract <A> voidlock(long position, long size, boolean shared, A attachment, CompletionHandler<FileLock,? super A> handler);
publicabstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
这个方法很简单,就是尝试去获取锁,如果该区域已被其他线程或其他应用锁住,那么立刻返回 null,否则返回 FileLock 对象。
AsynchronousServerSocketChannel
package com.javadoop.aio; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousServerSocketChannel; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; public classServer{ publicstaticvoidmain(String[] args) throws IOException { // 实例化,并监听端口 AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080)); // 自己定义一个 Attachment 类,用于传递一些信息 Attachment att = new Attachment(); att.setServer(server); server.accept(att, new CompletionHandler<AsynchronousSocketChannel, Attachment>() { @Override publicvoidcompleted(AsynchronousSocketChannel client, Attachment att) { try { SocketAddress clientAddr = client.getRemoteAddress(); System.out.println("收到新的连接:" + clientAddr); // 收到新的连接后,server 应该重新调用 accept 方法等待新的连接进来 att.getServer().accept(att, this); Attachment newAtt = new Attachment(); newAtt.setServer(server); newAtt.setClient(client); newAtt.setReadMode(true); newAtt.setBuffer(ByteBuffer.allocate(2048)); // 这里也可以继续使用匿名实现类,不过代码不好看,所以这里专门定义一个类 client.read(newAtt.getBuffer(), newAtt, new ChannelHandler()); } catch (IOException ex) { ex.printStackTrace(); } } @Override publicvoidfailed(Throwable t, Attachment att) { System.out.println("accept failed"); } }); // 为了防止 main 线程退出 try { Thread.currentThread().join(); } catch (InterruptedException e) { } } }
package com.javadoop.aio; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.CompletionHandler; import java.nio.charset.Charset; public classChannelHandlerimplementsCompletionHandler<Integer, Attachment> { @Override publicvoidcompleted(Integer result, Attachment att) { if (att.isReadMode()) { // 读取来自客户端的数据 ByteBuffer buffer = att.getBuffer(); buffer.flip(); byte bytes[] = new byte[buffer.limit()]; buffer.get(bytes); String msg = new String(buffer.array()).toString().trim(); System.out.println("收到来自客户端的数据: " + msg); // 响应客户端请求,返回数据 buffer.clear(); buffer.put("Response from server!".getBytes(Charset.forName("UTF-8"))); att.setReadMode(false); buffer.flip(); // 写数据到客户端也是异步 att.getClient().write(buffer, att, this); } else { // 到这里,说明往客户端写数据也结束了,有以下两种选择: // 1. 继续等待客户端发送新的数据过来 // att.setReadMode(true); // att.getBuffer().clear(); // att.getClient().read(att.getBuffer(), att, this); // 2. 既然服务端已经返回数据给客户端,断开这次的连接 try { att.getClient().close(); } catch (IOException e) { } } } @Override publicvoidfailed(Throwable t, Attachment att) { System.out.println("连接断开"); } }
public classAttachment{ private AsynchronousServerSocketChannel server; private AsynchronousSocketChannel client; private boolean isReadMode; private ByteBuffer buffer; // getter & setter }
AsynchronousSocketChannel
package com.javadoop.aio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.nio.charset.Charset; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; public classClient{ publicstaticvoidmain(String[] args) throws Exception { AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); // 来个 Future 形式的 Future<?> future = client.connect(new InetSocketAddress(8080)); // 阻塞一下,等待连接成功 future.get(); Attachment att = new Attachment(); att.setClient(client); att.setReadMode(false); att.setBuffer(ByteBuffer.allocate(2048)); byte[] data = "I am obot!".getBytes(); att.getBuffer().put(data); att.getBuffer().flip(); // 异步发送数据到服务端 client.write(att.getBuffer(), att, new ClientChannelHandler()); // 这里休息一下再退出,给出足够的时间处理数据 Thread.sleep(2000); } }
package com.javadoop.aio; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.CompletionHandler; import java.nio.charset.Charset; public classClientChannelHandlerimplementsCompletionHandler<Integer, Attachment> { @Override publicvoidcompleted(Integer result, Attachment att) { ByteBuffer buffer = att.getBuffer(); if (att.isReadMode()) { // 读取来自服务端的数据 buffer.flip(); byte[] bytes = new byte[buffer.limit()]; buffer.get(bytes); String msg = new String(bytes, Charset.forName("UTF-8")); System.out.println("收到来自服务端的响应数据: " + msg); // 接下来,有以下两种选择: // 1. 向服务端发送新的数据 // att.setReadMode(false); // buffer.clear(); // String newMsg = "new message from client"; // byte[] data = newMsg.getBytes(Charset.forName("UTF-8")); // buffer.put(data); // buffer.flip(); // att.getClient().write(buffer, att, this); // 2. 关闭连接 try { att.getClient().close(); } catch (IOException e) { } } else { // 写操作完成后,会进到这里 att.setReadMode(true); buffer.clear(); att.getClient().read(buffer, att, this); } } @Override publicvoidfailed(Throwable t, Attachment att) { System.out.println("服务器无响应"); } }
Asynchronous Channel Groups
-
java.nio.channels.DefaultThreadPool.threadFactory此系统变量用于设置 ThreadFactory,它应该是 java.util.concurrent.ThreadFactory 实现类的全限定类名。一旦我们指定了这个 ThreadFactory 以后,group 中的线程就会使用该类产生。
-
java.nio.channels.DefaultThreadPool.initialSize此系统变量也很好理解,用于设置线程池的初始大小。
-
AsynchronousChannelGroup.withCachedThreadPool(ExecutorService executor, int initialSize)
-
AsynchronousChannelGroup.withFixedThreadPool(int nThreads, ThreadFactory threadFactory)
-
AsynchronousChannelGroup.withThreadPool(ExecutorService executor)
AsynchronousChannelGroup group = AsynchronousChannelGroup .withFixedThreadPool(10, Executors.defaultThreadFactory()); AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group); AsynchronousSocketChannel client = AsynchronousSocketChannel.open(group);
publicstatic AsynchronousFileChannel open(Path file, Set<? extends OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) { ... }
小结
学Java,请关注公众号:Java后端