BIO:
BIO是阻塞IO,体现在一个线程调用IO的时候,会挂起等待,然后Thread会进入blocked状态;这样线程资源就会被闲置,造成资源浪费,通常一个系统线程数是有限的,而且,Thread进入内核态也是很大的性能开销。而阻塞方式,意味着BIO必然是一个同步IO。
BIO还有一个显著的特点是面向流式Stream编程,特点是实现简单,但也意味着拓展性差。
NIO:
NIO,通常实现为同步非阻塞IO,同步意味着不会产生会调,需要线程自身去同步IO是否完成,而非阻塞就是线程会立刻返回。
相对于BIO面向流式抽象思想编程,NIO是面向管道编程的,例如在Java中必谈的三个封装类Buffer、Channel、Sellector,就是管道编程的体现,Java1.4后提供的非阻塞 IO 的核心在于使用一个 Selector 来管理多个通道,可以是 SocketChannel,也可以是 ServerSocketChannel,将各个通道注册到 Selector 上,指定监听的事件。之后可以只用一个线程来轮询这个 Selector,看看上面是否有通道是准备好的,当通道准备好可读或可写,然后才去开始真正的读写,这样速度就很快了。我们就完全没有必要给每个通道都起一个线程。如下面代码所示:
1 package com.mobisummer.spider.slave.task.aliexpress.region; 2 3 import java.io.IOException; 4 import java.net.InetSocketAddress; 5 import java.net.ServerSocket; 6 import java.nio.ByteBuffer; 7 import java.nio.channels.SelectionKey; 8 import java.nio.channels.Selector; 9 import java.nio.channels.ServerSocketChannel; 10 import java.nio.channels.SocketChannel; 11 import java.util.Iterator; 12 import java.util.Set; 13 14 public class PlainNioServer { 15 16 public void serve(int port) throws IOException { 17 18 ServerSocketChannel serverChannel = ServerSocketChannel.open(); 19 serverChannel.configureBlocking(false); 20 ServerSocket ssocket = serverChannel.socket(); 21 InetSocketAddress address = new InetSocketAddress(port); 22 ssocket.bind(address); 23 Selector selector = Selector.open(); 24 serverChannel.register(selector, SelectionKey.OP_ACCEPT); 25 final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes()); 26 27 for (; ; ) { 28 try { 29 selector.select(); 30 } catch (IOException ex) { 31 ex.printStackTrace(); 32 // handle exception 33 break; 34 } 35 Set<SelectionKey> readyKeys = selector.selectedKeys(); 36 Iterator<SelectionKey> iterator = readyKeys.iterator(); 37 while (iterator.hasNext()) { 38 SelectionKey key = iterator.next(); 39 iterator.remove(); 40 try { 41 if (key.isAcceptable()) { 42 ServerSocketChannel server = (ServerSocketChannel) key.channel(); 43 SocketChannel client = server.accept(); 44 client.configureBlocking(false); 45 client.register(selector, 46 SelectionKey.OP_WRITE | SelectionKey.OP_READ, 47 msg.duplicate()); 48 System.out.println("Accepted connection from " + client); 49 } 50 if (key.isWritable()) { 51 SocketChannel client = (SocketChannel) key.channel(); 52 ByteBuffer buffer = (ByteBuffer) key.attachment(); 53 while (buffer.hasRemaining()) { 54 if (client.write(buffer) == 0) { 55 break; 56 } 57 } 58 client.close(); 59 } 60 } catch (IOException ex) { 61 key.cancel(); 62 try { 63 key.channel().close(); 64 } catch (IOException cex) { 65 // ignore on close 66 } 67 } 68 } 69 } 70 } 71 }
对于并发数量大但处理的任务又十分快速的时候用处十分显著,代替了之前的利用多线程解决业务问题的方案,就是利用单线程以及底层epoll或者poll原理完成了单线程处理多任务的方案,理论上至少我们想到了减少线程切换的开支,而由内核去改变IO状态。
【说说实现】
NIO 中 Selector 是对底层操作系统实现的一个抽象,管理通道状态其实都是底层系统实现的,在不同系统下的实现会不同,是自动选择的,可能的实现方式如下:
select:上世纪 80 年代的事情了,它支持注册 FD_SETSIZE(1024) 个 socket,在那个年代肯定是够用的。
poll:1997 年,出现了 poll 作为 select 的替代者,最大的区别就是,poll 不再限制 socket 数量。
select 和 poll 都有一个共同的问题,那就是它们都只会告诉你有几个通道准备好了,但是不会告诉你具体是哪几个通道。所以,一旦知道有通道准备好以后,自己还是需要进行一次扫描,显然这个不太好,通道少的时候还行,一旦通道的数量是几十万个以上的时候,扫描一次的时间都很可观了,时间复杂度 O(n)。所以,后来才催生了以下实现。
epoll:2002 年随 Linux 内核 2.5.44 发布,epoll 能直接返回具体的准备好的通道,时间复杂度 O(1)。那么这个epoll是怎么的原理呢?这就涉及操作系统的中断了,在内核的最底层是中断,类似系统回调的机制。网卡设备对应一个中断号, 当网卡收到网络端的消息的时候会向CPU发起中断请求, 然后CPU处理该请求. 通过驱动程序 进而操作系统得到通知, 系统然后通知epoll, epoll改变阻塞状态。
除了 Linux 中的 epoll,2000 年 FreeBSD 出现了 Kqueue,还有就是,Solaris 中有 /dev/poll。
前面说了那么多实现,但是没有出现 Windows,Windows 平台的非阻塞 IO 使用 select,我们也不必觉得 Windows 很落后,在 Windows 中 IOCP 提供的异步 IO 是比较强大的。
AIO:
异步这个词,我想对于绝大多数开发者来说都很熟悉,很多场景下我们都会使用异步。对于我而言比较有意义的事情就是发现我所在公司自己做的底层框架Lwmf,自己做了一个声称为AIO的实现,只不过是封装了一层罢。
通常,我们会有一个线程池用于执行异步任务,提交任务的线程将任务提交到线程池就可以立马返回,不必等到任务真正完成。如果想要知道任务的执行结果,通常是通过传递一个回调函数的方式,任务结束后去调用这个函数。
同样的原理,Java 中的异步 IO 也是一样的,都是由一个线程池来负责执行任务,然后使用回调或自己去查询结果,所以这里涉及了两个实现方式,在Java中就是注册回调函数和使用异步任务返回的Feature实例。
干货在这里:对象是过程的抽象,而线程是调度的抽象;所以,设计异步IO的时候,需要把线程控制的牢牢的,才能更稳健的设计哦。
最后,不得不提一下的就是Reactor模型和Netty框架了!但不是本文重点,但这确实是java中优秀的NIO实现