Java NIO详解

底层I/O
  在说Java NIO之前先介绍一下与系统相关的一些底层I/O细节,通常为了系统的安全,用户进程是无法直接操作I/O设备的,必须通过调用系统内核来协助完成I/O动作,在系统内核中为每个I/O设备维护着一个buffer。整个流程如下图所示:用户进程发起请求recvFrom,内核接受请求,从I/O设备中读取数据到内核buffer中,然后将buffer中的数据copy到用户进程的地址空间(可以理解成内存),用户进程从用户空间中获取数据后再响应客户端

  在整个请求过程中,将数据(从磁盘读取或从网卡读取)读至buffer需要时间,从内核buffer复制到用户空间也需要时间。因此根据在这两段时间内用户进程等待方式的不同,I/O动作可以分为以下5类:
1)阻塞I/O (Blocking I/O):在IO执行的两个阶段用户进程一直处于block状态直到内核将数据拷贝到用户缓冲区,返回结果后才会解除block状态。如图

  当用户进程调用了recvfrom这个系统调用后,内核就开始了IO的第一个阶段:等待数据准备。对于网络IO来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除block的状态,重新运行起来。
  在JDK1.4之前,所有的IO都是阻塞I/O,如下面的is.read(),该方法的阻塞流程是这样的:客户端通过输出流发送数据-->网络传输-->服务器网卡接到数据-->读取数据到系统内核-->复制到用户进程空间(内存)-->read方法从内存中读取数据,返回。在这样的整个过程中该线程都处理阻塞状态。

public static void bio() throws IOException {
	ServerSocket ss = new ServerSocket(9191);
	Socket socket = ss.accept();
    InputStream is = socket.getInputStream();
	byte[] b = new byte[1024];
	is.read(b);
}

        在Java中,BIO通常配合多线程或线程池一起使用,如早期的Tomcat

    private static final int DEFAULT_POO_SIZE = 10;
    	
    private static final ExecutorService pool = Executors.newFixedThreadPool(DEFAULT_POO_SIZE);

    public static void bio() throws Exception {
	ServerSocket ss = new ServerSocket(9191);
	while (true) {
		Socket socket = ss.accept();
		pool.execute(() -> {
			//doing something (socket)
		});
	}
}

        相当于每接收到一个客户端请求,就分配一个新的线程去处理,随着访问量的增加,线程数量有限的情况下,阻塞仍然不可避免,但如果增加线程数量,则线程间切换的成本也会相应的提高,所以在访问量大的情况下可以使用多路复用I/O进一步提高利用率。

 2)非阻塞I/O (Non-Blocking I/O):进程在发送请求后会立即收到一个结果,如果此时内核中数据尚未准备好,那么这个结果代表一个错误信息,通常情况下,进程需要利用轮询的方式来检测该数据是否就绪,如果最终内核的数据准备好了,并且又再次收到了用户进程的调用请求,那么它马上就将数据拷贝到用户内存,然后返回。这里说的轮询是指在应用程序中循环调用,是一种比较浪费CPU时间的操作,但这种模式偶尔会遇到。

3)I/O复用(I/O Multiplexing):多路复用的基本原理依赖于select/epoll函数,select/epoll会不断的轮询其负责的所有socket,当某个socket有数据到达了,就通知用户进程(与上面的区别在于,轮询是由操作系统发起的,而不是我们的应用程序)。所以这就决定了select/epoll的优势并不是对于单个连接能处理得更快,而是在于系统级别的连接过滤(及时过滤出可以处理的连接),从而可以一定程序上应对更多的连接。如下图

  当用户进程调用了select,进程处于block状态,内核会轮询“监视”select负责的socket,只要有任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。在整个过程中,用户进程也是一直被block的,只不过进程是被select这个函数block,而不是被Socket IO给block。
  虽然多路复用IO和阻塞IO在整个过程中都会被阻塞,但是select的优势在于在一个进程(或线程)中可以同时处理多个connection,更适应连接量大的场景,可以减少需要的线程数量,从而降低线程间的切换消耗。但如果处理的连接数不是很高的话,使用select就并不一定比【多线程 + 阻塞IO】要快了,原因在于select需要使用两个system call (select 和 recvfrom),而bio只需要调用一个recvfrom。  

4)信号驱动I/O

  进程向内核注册一个IO信号事件,在数据可操作时内核将通过SIGIO信号通知应用进程,应用进程就可以在优先定义好的信号处理程序中调用recvfrom来读数据(相当于回调函数)。整个过程在第一阶段并会不block,只在第二阶段(将数据从内核复制到应该进程的空间)会产生block。

以上四种,虽然在第一阶段阻塞情况各不相同,但在第二阶段数据拷贝期间都会block,所以都可以看成是同步IO,这也是同步IO和异步IO的主要区别。
5)异步AIO
 
 进程通过系统调用通知内核启动某个操作并在整个操作(包括第二阶段的操作)完成后通知进程。原理与信号驱动I/O相似,主要区别在于,前者告诉我们何时可以开始拷贝,而异步I/O表示何时拷贝完成。

总结
阻塞IO:两个阶段都阻塞
非阻塞IO:在第一阶段,进程不断的轮询直到数据准备好,第二阶段还是阻塞的
IO复用:在第一阶段,由内核轮询所有监视的socket,当有IO准备就绪时,通知进程。第二阶段进程通过recvform来拷贝数据。两个阶段都阻塞,优势在于可以连接过滤,从而可以应对更多的连接。
信号IO:进程注册IO事件信号,内核在数据准备完毕后通过响应信号,通知进程,只在第二阶段阻塞。
异步IO:两阶段都不会阻塞

select/poll/epoll说明:select/poll/epoll链接好文

Java对NIO的支持
  nio即new I/O,在JDK1.4之后才有(JDK1.6版本后使用epoll替代了传统的select/poll,更加提升了NIO通信的性能),与原IO的主要区别在于,NIO是一种非阻塞式I/O,工作原理是通过由Selector来集中管理和分发所有的IO事件,事件到来时,执行相应监听事件,这其实就是I/O多路复用模式。事件主要有以下四类

OP_ACCEPT服务端接收客户端连接
OP_CONNECT客户端新开连接事件
OP_READ读事件
OP_WRITE写事件

从编码上来说NIO面向是缓冲区,而且是通道是双向的。而原IO面向流且是单向的,要么是输出流要么是输入流。

选择器(Selector)
  
选择器用于监听通道事件,通过向selector中注册多个通道,可以在单个线程中可以监听多个数据通道。与selector配合的通道必须是非阻塞通道,对于阻塞通道,在注册时将抛出异常:IllegalBlockingModeException
​​​​​​​

通道(Channel)
  
通道是一个用于 I/O 操作的连接,可以是一个网络连接、或一个文件描述符等等。通道总是从buffer中读取数据或将数据写入到buffer中。常用的通道类型有以下几种

  • FileChannel:从文件中读写数据,这被设计成一个阻塞通道(因为认为file操作不需要非阻塞),不能与与selector配合使用。  
  • DatagramChannel:通过UDP向网络连接的两端读写数据
  • SocketChannel:通过TCP向网络连接的两端读写数据,代表客户端到服务端的一个连接。  
  • ServerSocketChannel:一个基于通道的Socket监听器,用来在服务端监听新进来的TCP连接,类似于Web服务器。

缓冲区(Buffer)
  
Buffer主要有以下个属性

  • capacity(容量):缓冲区大小
  • position(偏移量):表示当前所处的读或写的位置,从0开始,所以最大值为capacity-1。
  • limit(上限):表示最多可以往buffer中写入或最多可从buffer中读取多少数据,在写模式下,该值等于capacity;读模式下该值等于写模式下的position。
  • mark(标记):用来标记一个位置,后续通过reset()可以恢复position到该位置,所以mark的位置<=position。

以上的4个属性:mark<=position<=limit<=capacity

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值