高并发Java:NIO和AIO

IO感觉上和多线程并没有多大关系,但是NIO改变了线程在应用层面使用的方式,也解决了一些实际的困难。而AIO是异步IO和前面的系列也有点关系。在此,为了学习和记录,也写一篇文章来介绍NIO和AIO。

1. 什么是NIO

NIO是New I/O的简称,与旧式的基于流的I/O方法相对,从名字看,它表示新的一套Java I/O标 准。它是在Java 1.4中被纳入到JDK中的,并具有以下特性:

  • NIO是基于块(Block)的,它以块为基本单位处理数据 (硬盘上存储的单位也是按Block来存储,这样性能上比基于流的方式要好一些)
  • 为所有的原始类型提供(Buffer)缓存支持
  • 增加通道(Channel)对象,作为新的原始 I/O 抽象
  • 支持锁(我们在平时使用时经常能看到会出现一些.lock的文件,这说明有线程正在使用这把锁,当线程释放锁时,会把这个文件删除掉,这样其他线程才能继续拿到这把锁)和内存映射文件的文件访问接口
  • 提供了基于Selector的异步网络I/O

高并发Java:NIO和AIO_第1张图片

所有的从通道中的读写操作,都要经过Buffer,而通道就是io的抽象,通道的另一端就是操纵的文件。

2. Buffer

高并发Java:NIO和AIO_第2张图片

Java中Buffer的实现。基本的数据类型都有它对应的Buffer

Buffer的简单使用例子:

   
   
   
   
  1. package test;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.nio.ByteBuffer;
  5. import java.nio.channels.FileChannel;
  6. public class Test {
  7. public static void main(String[] args) throws Exception {
  8. FileInputStream fin = new FileInputStream(new File(
  9. "d:\\temp_buffer.tmp"));
  10. FileChannel fc = fin.getChannel();
  11. ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
  12. fc.read(byteBuffer);
  13. fc.close();
  14. byteBuffer.flip();//读写转换
  15. }
  16. }

总结下使用的步骤是:

  1. 得到Channel

  2. 申请Buffer

  3. 建立Channel和Buffer的读/写关系

  4. 关闭

下面的例子是使用NIO来复制文件:

   
   
   
   
  1. public static void nioCopyFile(String resource, String destination)
  2. throws IOException {
  3. FileInputStream fis = new FileInputStream(resource);
  4. FileOutputStream fos = new FileOutputStream(destination);
  5. FileChannel readChannel = fis.getChannel(); // 读文件通道
  6. FileChannel writeChannel = fos.getChannel(); // 写文件通道
  7. ByteBuffer buffer = ByteBuffer.allocate(1024); // 读入数据缓存
  8. while (true) {
  9. buffer.clear();
  10. int len = readChannel.read(buffer); // 读入数据
  11. if (len == -1) {
  12. break; // 读取完毕
  13. }
  14. buffer.flip();
  15. writeChannel.write(buffer); // 写入文件
  16. }
  17. readChannel.close();
  18. writeChannel.close();
  19. }

Buffer中有3个重要的参数:位置(position)、容量(capactiy)和上限(limit)

高并发Java:NIO和AIO_第3张图片

这里要区别下容量和上限,比如一个Buffer有10KB,那么10KB就是容量,我将5KB的文件读到Buffer中,那么上限就是5KB。

下面举个例子来理解下这3个重要的参数:

   
   
   
   
  1. public static void main(String[] args) throws Exception {
  2. ByteBuffer b = ByteBuffer.allocate(15); // 15个字节大小的缓冲区
  3. System.out.println("limit=" + b.limit() + " capacity=" + b.capacity()
  4. + " position=" + b.position());
  5. for (int i = 0; i < 10; i++) {
  6. // 存入10个字节数据
  7. b.put((byte) i);
  8. }
  9. System.out.println("limit=" + b.limit() + " capacity=" + b.capacity()
  10. + " position=" + b.position());
  11. b.flip(); // 重置position
  12. System.out.println("limit=" + b.limit() + " capacity=" + b.capacity()
  13. + " position=" + b.position());
  14. for (int i = 0; i < 5; i++) {
  15. System.out.print(b.get());
  16. }
  17. System.out.println();
  18. System.out.println("limit=" + b.limit() + " capacity=" + b.capacity()
  19. + " position=" + b.position());
  20. b.flip();
  21. System.out.println("limit=" + b.limit() + " capacity=" + b.capacity()
  22. + " position=" + b.position());
  23. }

整个过程如图:

高并发Java:NIO和AIO_第4张图片

此时position从0到10,capactiy和limit不变。

高并发Java:NIO和AIO_第5张图片

该操作会重置position,通常,将buffer从写模式转换为读 模式时需要执行此方法 flip()操作不仅重置了当前的position为0,还将limit设置到当前position的位置 。

limit的意义在于,来确定哪些数据是有意义的,换句话说,从position到limit之间的数据才是有意义的数据,因为是上次操作的数据。所以flip操作往往是读写转换的意思。

高并发Java:NIO和AIO_第6张图片

意义同上。

而Buffer中大多数的方法都是去改变这3个参数来达到某些功能的:

   
   
   
   
  1. public final Buffer rewind()

将position置零,并清除标志位(mark)

   
   
   
   
  1. public final Buffer clear()

将position置零,同时将limit设置为capacity的大小,并清除了标志mark

   
   
   
   
  1. public final Buffer flip()

先将limit设置到position所在位置,然后将position置零,并清除标志位mark,通常在读写转换时使用

文件映射到内存

   
   
   
   
  1. public static void main(String[] args) throws Exception {
  2. RandomAccessFile raf = new RandomAccessFile("C:\\mapfile.txt", "rw");
  3. FileChannel fc = raf.getChannel();
  4. // 将文件映射到内存中
  5. MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0,
  6. raf.length());
  7. while (mbb.hasRemaining()) {
  8. System.out.print((char) mbb.get());
  9. }
  10. mbb.put(0, (byte) 98); // 修改文件
  11. raf.close();
  12. }

对MappedByteBuffer的修改就相当于修改文件本身,这样操作的速度是很快的。

3. Channel

多线程网络服务器的一般结构:

高并发Java:NIO和AIO_第7张图片

简单的多线程服务器:

   
   
   
   
  1. public static void main(String[] args) throws Exception {
  2. ServerSocket echoServer = null;
  3. Socket clientSocket = null;
  4. try {
  5. echoServer = new ServerSocket(8000);
  6. } catch (IOException e) {
  7. System.out.println(e);
  8. }
  9. while (true) {
  10. try {
  11. clientSocket = echoServer.accept();
  12. System.out.println(clientSocket.getRemoteSocketAddress()
  13. + " connect!");
  14. tp.execute(new HandleMsg(clientSocket));
  15. } catch (IOException e) {
  16. System.out.println(e);
  17. }
  18. }
  19. }

功能就是服务器端读到什么数据,就向客户端回写什么数据。

这里的tp是一个线程池,HandleMsg是处理消息的类。

   
   
   
   
  1. static class HandleMsg implements Runnable{
  2. 省略部分信息
  3. public void run(){
  4. try {
  5. is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
  6. os = new PrintWriter(clientSocket.getOutputStream(), true);
  7. // 从InputStream当中读取客户端所发送的数据
  8. String inputLine = null;
  9. long b=System. currentTimeMillis ();
  10. while ((inputLine = is.readLine()) != null)
  11. {
  12. os.println(inputLine);
  13. }
  14. long e=System. currentTimeMillis ();
  15. System. out.println ("spend:"+(e - b)+" ms ");
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }finally
  19. {
  20. 关闭资源
  21. }
  22. }
  23. }

客户端:

   
   
   
   
  1. public static void main(String[] args) throws Exception {
  2. Socket client = null;
  3. PrintWriter writer = null;
  4. BufferedReader reader = null;
  5. try {
  6. client = new Socket();
  7. client.connect(new InetSocketAddress("localhost", 8000));
  8. writer = new PrintWriter(client.getOutputStream(), true);
  9. writer.println("Hello!");
  10. writer.flush();
  11. reader = new BufferedReader(new InputStreamReader(
  12. client.getInputStream()));
  13. System.out.println("from server: " + reader.readLine());
  14. } catch (Exception e) {
  15. } finally {
  16. // 省略资源关闭
  17. }
  18. }

以上的网络编程是很基本的,使用这种方式,会有一些问题:

为每一个客户端使用一个线程,如果客户端出现延时等异常,线程可能会被占用很长时间。因为数据的准备和读取都在这个线程中。此时,如果客户端数量众多,可能会消耗大量的系统资源。

解决方案:

使用非阻塞的NIO (读取数据不等待,数据准备好了再工作)

为了体现NIO使用的高效。

这里先模拟一个低效的客户端来模拟因网络而延时的情况:

   
   
   
   
  1. private static ExecutorService tp= Executors.newCachedThreadPool();
  2. private static final int sleep_time=1000*1000*1000;
  3. public static class EchoClient implements Runnable{
  4. public void run(){
  5. try {
  6. client = new Socket();
  7. client.connect(new InetSocketAddress("localhost", 8000));
  8. writer = new PrintWriter(client.getOutputStream(), true);
  9. writer.print("H");
  10. LockSupport.parkNanos(sleep_time);
  11. writer.print("e");
  12. LockSupport.parkNanos(sleep_time);
  13. writer.print("l");
  14. LockSupport.parkNanos(sleep_time);
  15. writer.print("l");
  16. LockSupport.parkNanos(sleep_time);
  17. writer.print("o");
  18. LockSupport.parkNanos(sleep_time);
  19. writer.print("!");
  20. LockSupport.parkNanos(sleep_time);
  21. writer.println();
  22. writer.flush();
  23. }catch(Exception e)
  24. {
  25. }
  26. }
  27. }

服务器端输出:

   
   
   
   
  1. spend:6000ms
  2. spend:6000ms
  3. spend:6000ms
  4. spend:6001ms
  5. spend:6002ms
  6. spend:6002ms
  7. spend:6002ms
  8. spend:6002ms
  9. spend:6003ms
  10. spend:6003ms

因为

   
   
   
   
  1. while ((inputLine = is.readLine()) != null)

是阻塞的,所以时间都花在等待中。

如果用NIO来处理这个问题会怎么做呢?

NIO有一个很大的特点就是:把数据准备好了再通知我

而Channel有点类似于流,一个Channel可以和文件或者网络Socket对应 。

高并发Java:NIO和AIO_第8张图片

selector是一个选择器,它可以选择某一个Channel,然后做些事情。

一个线程可以对应一个selector,而一个selector可以轮询多个Channel,而每个Channel对应了一个Socket。

与上面一个线程对应一个Socket相比,使用NIO后,一个线程可以轮询多个Socket。

当selector调用select()时,会查看是否有客户端准备好了数据。当没有数据被准备好时,select()会阻塞。平时都说NIO是非阻塞的,但是如果没有数据被准备好还是会有阻塞现象。

当有数据被准备好时,调用完select()后,会返回一个SelectionKey,SelectionKey表示在某个selector上的某个Channel的数据已经被准备好了。

只有在数据准备好时,这个Channel才会被选择。

这样NIO实现了一个线程来监控多个客户端。

而刚刚模拟的网络延迟的客户端将不会影响NIO下的线程,因为某个Socket网络延迟时,数据还未被准备好,selector是不会选择它的,而会选择其他准备好的客户端。

selectNow()与select()的区别在于,selectNow()是不阻塞的,当没有客户端准备好数据时,selectNow()不会阻塞,将返回0,有客户端准备好数据时,selectNow()返回准备好的客户端的个数。

主要代码:

   
   
   
   
  1. package test;
  2. import java.net.InetAddress;
  3. import java.net.InetSocketAddress;
  4. import java.net.Socket;
  5. import java.nio.ByteBuffer;
  6. import java.nio.channels.SelectionKey;
  7. import java.nio.channels.Selector;
  8. import java.nio.channels.ServerSocketChannel;
  9. import java.nio.channels.SocketChannel;
  10. import java.nio.channels.spi.AbstractSelector;
  11. import java.nio.channels.spi.SelectorProvider;
  12. import java.util.HashMap;
  13. import java.util.Iterator;
  14. import java.util.LinkedList;
  15. import java.util.Map;
  16. import java.util.Set;
  17. import java.util.concurrent.ExecutorService;
  18. import java.util.concurrent.Executors;
  19. public class MultiThreadNIOEchoServer {
  20. public static Map<Socket, Long> geym_time_stat = new HashMap<Socket, Long>();
  21. class EchoClient {
  22. private LinkedList<ByteBuffer> outq;
  23. EchoClient() {
  24. outq = new LinkedList<ByteBuffer>();
  25. }
  26. public LinkedList<ByteBuffer> getOutputQueue() {
  27. return outq;
  28. }
  29. public void enqueue(ByteBuffer bb) {
  30. outq.addFirst(bb);
  31. }
  32. }
  33. class HandleMsg implements Runnable {
  34. SelectionKey sk;
  35. ByteBuffer bb;
  36. public HandleMsg(SelectionKey sk, ByteBuffer bb) {
  37. super();
  38. this.sk = sk;
  39. this.bb = bb;
  40. }
  41. @Override
  42. public void run() {
  43. // TODO Auto-generated method stub
  44. EchoClient echoClient = (EchoClient) sk.attachment();
  45. echoClient.enqueue(bb);
  46. sk.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
  47. selector.wakeup();
  48. }
  49. }
  50. private Selector selector;
  51. private ExecutorService tp = Executors.newCachedThreadPool();
  52. private void startServer() throws Exception {
  53. selector = SelectorProvider.provider().openSelector();
  54. ServerSocketChannel ssc = ServerSocketChannel.open();
  55. ssc.configureBlocking(false);
  56. InetSocketAddress isa = new InetSocketAddress(8000);
  57. ssc.socket().bind(isa);
  58. // 注册感兴趣的事件,此处对accpet事件感兴趣
  59. SelectionKey acceptKey = ssc.register(selector, SelectionKey.OP_ACCEPT);
  60. for (;;) {
  61. selector.select();
  62. Set readyKeys = selector.selectedKeys();
  63. Iterator i = readyKeys.iterator();
  64. long e = 0;
  65. while (i.hasNext()) {
  66. SelectionKey sk = (SelectionKey) i.next();
  67. i.remove();
  68. if (sk.isAcceptable()) {
  69. doAccept(sk);
  70. } else if (sk.isValid() && sk.isReadable()) {
  71. if (!geym_time_stat.containsKey(((SocketChannel) sk
  72. .channel()).socket())) {
  73. geym_time_stat.put(
  74. ((SocketChannel) sk.channel()).socket(),
  75. System.currentTimeMillis());
  76. }
  77. doRead(sk);
  78. } else if (sk.isValid() && sk.isWritable()) {
  79. doWrite(sk);
  80. e = System.currentTimeMillis();
  81. long b = geym_time_stat.remove(((SocketChannel) sk
  82. .channel()).socket());
  83. System.out.println("spend:" + (e - b) + "ms");
  84. }
  85. }
  86. }
  87. }
  88. private void doWrite(SelectionKey sk) {
  89. // TODO Auto-generated method stub
  90. SocketChannel channel = (SocketChannel) sk.channel();
  91. EchoClient echoClient = (EchoClient) sk.attachment();
  92. LinkedList<ByteBuffer> outq = echoClient.getOutputQueue();
  93. ByteBuffer bb = outq.getLast();
  94. try {
  95. int len = channel.write(bb);
  96. if (len == -1) {
  97. disconnect(sk);
  98. return;
  99. }
  100. if (bb.remaining() == 0) {
  101. outq.removeLast();
  102. }
  103. } catch (Exception e) {
  104. // TODO: handle exception
  105. disconnect(sk);
  106. }
  107. if (outq.size() == 0) {
  108. sk.interestOps(SelectionKey.OP_READ);
  109. }
  110. }
  111. private void doRead(SelectionKey sk) {
  112. // TODO Auto-generated method stub
  113. SocketChannel channel = (SocketChannel) sk.channel();
  114. ByteBuffer bb = ByteBuffer.allocate(8192);
  115. int len;
  116. try {
  117. len = channel.read(bb);
  118. if (len < 0) {
  119. disconnect(sk);
  120. return;
  121. }
  122. } catch (Exception e) {
  123. // TODO: handle exception
  124. disconnect(sk);
  125. return;
  126. }
  127. bb.flip();
  128. tp.execute(new HandleMsg(sk, bb));
  129. }
  130. private void disconnect(SelectionKey sk) {
  131. // TODO Auto-generated method stub
  132. //省略略干关闭操作
  133. }
  134. private void doAccept(SelectionKey sk) {
  135. // TODO Auto-generated method stub
  136. ServerSocketChannel server = (ServerSocketChannel) sk.channel();
  137. SocketChannel clientChannel;
  138. try {
  139. clientChannel = server.accept();
  140. clientChannel.configureBlocking(false);
  141. SelectionKey clientKey = clientChannel.register(selector,
  142. SelectionKey.OP_READ);
  143. EchoClient echoClinet = new EchoClient();
  144. clientKey.attach(echoClinet);
  145. InetAddress clientAddress = clientChannel.socket().getInetAddress();
  146. System.out.println("Accepted connection from "
  147. + clientAddress.getHostAddress());
  148. } catch (Exception e) {
  149. // TODO: handle exception
  150. }
  151. }
  152. public static void main(String[] args) {
  153. // TODO Auto-generated method stub
  154. MultiThreadNIOEchoServer echoServer = new MultiThreadNIOEchoServer();
  155. try {
  156. echoServer.startServer();
  157. } catch (Exception e) {
  158. // TODO: handle exception
  159. }
  160. }
  161. }

代码仅作参考,主要的特点是,对不同事件的感兴趣来做不同的事。

当用之前模拟的那个延迟的客户端时,这次的时间消耗就在2ms到11ms之间了。性能提升是很明显的。

总结:

1. NIO会将数据准备好后,再交由应用进行处理,数据的读取/写入过程依然在应用线程中完成,只是将等待的时间剥离到单独的线程中去。

  1. 节省数据准备时间(因为Selector可以复用)

5. AIO

AIO的特点:

1. 读完了再通知我

2. 不会加快IO,只是在读完后进行通知

3. 使用回调函数,进行业务处理

AIO的相关代码:

AsynchronousServerSocketChannel

   
   
   
   
  1. server = AsynchronousServerSocketChannel.open().bind( new InetSocketAddress (PORT));

使用server上的accept方法

   
   
   
   
  1. public abstract <A> void accept(A attachment,CompletionHandler<AsynchronousSocketChannel,? super A> handler);

CompletionHandler为回调接口,当有客户端accept之后,就做handler中的事情。

示例代码:

   
   
   
   
  1. server.accept(null,
  2. new CompletionHandler<AsynchronousSocketChannel, Object>() {
  3. final ByteBuffer buffer = ByteBuffer.allocate(1024);
  4. public void completed(AsynchronousSocketChannel result,
  5. Object attachment) {
  6. System.out.println(Thread.currentThread().getName());
  7. Future<Integer> writeResult = null;
  8. try {
  9. buffer.clear();
  10. result.read(buffer).get(100, TimeUnit.SECONDS);
  11. buffer.flip();
  12. writeResult = result.write(buffer);
  13. } catch (InterruptedException | ExecutionException e) {
  14. e.printStackTrace();
  15. } catch (TimeoutException e) {
  16. e.printStackTrace();
  17. } finally {
  18. try {
  19. server.accept(null, this);
  20. writeResult.get();
  21. result.close();
  22. } catch (Exception e) {
  23. System.out.println(e.toString());
  24. }
  25. }
  26. }
  27. @Override
  28. public void failed(Throwable exc, Object attachment) {
  29. System.out.println("failed: " + exc);
  30. }
  31. });

这里使用了Future来实现即时返回,关于Future请参考[上一篇][Link 1]

在理解了NIO的基础上,看AIO,区别在于AIO是等读写过程完成后再去调用回调函数。

NIO是同步非阻塞的

AIO是异步非阻塞的

由于NIO的读写过程依然在应用线程里完成,所以对于那些读写过程时间长的,NIO就不太适合。

而AIO的读写过程完成后才被通知,所以AIO能够胜任那些重量级,读写过程长的任务。

你可能感兴趣的:(java)