传统的BIO方式是基于流行进行读写的,而且是阻塞的,整体性能比较差。为了提高I/O性能,JDK与1.4版本引入NIo,他弥补了原来BIO方式的不足,在标准的Java代码中提供了高速、面向块的I/O。通过定义包含数据的类以及块的形式处理数据,NIO可以再不编写表弟代码的气哭下利用底层优化,这是BIO无法做到的。
与BIO相比,NIO有如下几个新的概念:
1.通道
通道(Channel)是对BIO中流的模拟,到任何目的地(或者来自任何地方)的所有数据都必须通过一个通道对象。
通道与流的不同之处在于通道是双向的。流只是在一个方向上移动(一个流要么用于读,要么用于写),而通道可以用于读、写或者同事用于读写。因为通道是双向的,所以他可以比流更好的反应底层操作系统的真实情况(特别是在UNIX模型中底层操作系统通道同样是双向的情况下)。
2.缓冲区
尽管通道用于读写数据,但是我们却并吧直接操作通道进行读写,而是通过缓冲区(Buffer)完成。缓冲区实质上是一个容器对象。发送给通道的所有对象都必须先放到缓冲区中,同样从通道中读取的任何数据都要先读到缓冲区中。
缓冲区体现了NIO与BIO的一个重要区别。在BIO中,读写可以直接操作流对象。简单讲,缓冲区通常是一个字节数组,也可以使用其他类型的数组。但是缓冲区不仅仅是一个数组,他提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
3.选择器
Java NIO提供了选择器组件(Selector)用于同时检测多个通道的事件以实现异步I/O。我们将感兴趣的事件注册到Selector上,当事件发生时可以通过Selector获得事件发生的通道,并进行相关的操作。
异步I/O的一个优势在于,他允许你同时根据大量的输入、输出执行I/O操作。同步I/O一般要借助于轮询,或者创建许许多多的线程以处理大量的链接。使用异步I/O,你可以监听任意数量的通道事件,不必轮询,也不必启动额外的线程。
由于Selector.select()方法是阻塞的,因此Tomcat采用轮询的方式进行处理,轮询线程称为Poller。每个Poller维护了一个Selector实例以及一个PollerEvent事件队列。每当接收到新的链接时,会将获得的SocketChannel对象封装为org.apache.tomcat.util.net.NioChannel,并且将其注册到Poller(创建一个PollerEvent实例,添加到事件队列)。
Poller运行时,首先将新添加到队列中的PollerEvent取出,并将SocketChannel的读事件(OP_READ)注册到Poller持有的Selector上,然后执行Selector.select。当捕获到读事件时,构造SocketProcessor,并提交到线程池进行请求处理。
为了提升对象的利用率,NioEndpoint分别为NioChannel和PollerEvent对象创建了缓存队列。当需要NioChannel和PollerEvent对象时,会检测缓存队列中是否存在可用对象,如果存在则从队列中取出对象并且重置,如果不存在则新建。
Poller在将SocketProcessor添加到请求处理线程池之前,会将接收到读事件的SocketChannel从Poller维护的Selector上取消注册,避免当前Socket多线程同时处理。而读写过程中的事件处理则是由NioSelectorPool完成的。事件变化如下图所示:
NioSelectorPool提供了一个Selector池,用于获取有效的Selector供SocketChannel读写使用。他由NioEndpoint维护,可以通过系统属性org.apache.tomcat.util.net.NioSelcetorShard配置是否在SocketChannel之间共享Selector,如果为true则所有SocketChannel均共享一个Selector实例,否则每一个SocketChannel使用不同的Selector,NioSelectorPool池维护的Selector实例数上限由属性maxSelectors确定。
NIOSelectorPool读信息分为阻塞和非阻塞两种方式:
同样,在NioEndpoint中写详细也分为阻塞和非阻塞两种方式:
综上可知,Tomcat在阻塞方式下读/写时并没有监听OP_READ/OP)WRITE事件,而是当第一次操作没有成功时再进行注册。这实际上是一种乐观设计,即假设网络大多数情况下是正常的。第一次操作不成功,则表明网络存在异常,此时再对事件进行监听。
NIO2是JDK7新增的文件及网络I/O特性,他继承自NIO,同时添加了众多特性及功能改进,其中最重要的即是对异步I/O(AIO)的支持。
1.通道
在AIO中,通道必须实现接口java.nio.channels.AsynchronousChannel。JDK7提供了3个通道实现类:java.nio.channels.AsynchronousFileChannel用于文件I/O,Java.nio.channels.AsyschronousServerSocketChannel和java.nio.channels.AsyschronousSocketChannel用于网络I/O。
2.缓冲区
AIO仍通过操作缓冲区完成数据的读写操作,此处不再描述。
3.Future和CompletionHandler
AIO操作存在两种操作方式:Future和CompletionHandler。
首先,AIO使用了Java并发包的API,无论接收Socket请求还是读写操作,均可以返回一个java.util.concurrent.Future对象来表示I/O处于等待状态。通过Future的方法,我们可以检测操作是否完成(isDone)、等待完成并取得操作结果(get)等。当接收请求(accept)结束时,Future.get返回值为AsynchronousSocketChannel;读写操作时(read/write),Future.get返回值为读写操作结果。
除了Future外,接收请求以及读写操作还支持指定一个java.nio.channels.CompletionHandler
比较两种操作方式,Future方式需要我们自己检测I/O操作状态或者直接通过Future.get()方法等待I/O操作结束,而CompletionHandler方式则由JDK检测I/O状态,我们需要实现每种操作状态的处理即可。在实际应用中,我们可以只采用Future方式或者CompletionHandler方式,也可以两者混合使用。
4.异步通道组
AIO新引入了异步通道组(Asynchronous Channel Group)的概念,每个异步通道均属于一个指定的异步通道组,同一个通道组内的通道共享一个线程池。线程池内的线程接收指令来执行I/O事件并将结果分发到CompletionHandler。异步通道组包括线程池以及所有通道工作线程共享的资源。通道生命周期受所属通道组影响,当通道组关闭后,通道也随着关闭。
在实际开发中,除了可以手动创建异步通道组外,JVM还维护了一个系统分为的通道组实例,作为默认通道组。如果创建通道时为指定通道组或者指定的通道组为空,那么将会使用默认通道组。
默认通道组通过两个系统属性进行配置。首先是java.nio.channels.DefaultThreadPool.threadFactory,该属性值为具体的java.util.concurrent.ThreadFactory类,由系统类加载器加载并且实例化,用于创建默认通道组线程池的线程。其次为java.nio.channels.DefaultThreadPool.initialSize,用于指定线程池的初始化大小。
如果默认 通道组不能满足需要,我们还可以通过AsynchronousChannelGroup的下列3个方法来创建自定义的通道组:
Tomcat中AIO的使用可以创建Nio2Endpoint。与BIO、NIO类似,Tomcat仍使用Acceptor线程池的方式接收客户端请求。在Acceptor中,采用Fy=uture方式进行请求接收。此外,Tomcat分别采用Future方式实现阻塞读写,采用CompletionHandler方式实现非阻塞读写。