NIO知识概括

135 阅读32分钟

@TOC

四种IO模型

Java IO读写原理:

  • 无论是Socket的读写还是文件的读写,在Java层面的应用开发或者是linux系统底层开发,都属于输入input和输出output的处理,简称为IO读写。在原理上和处理流程上,都是一致的。区别在于参数的不同。
  • 用户程序进行IO的读写,基本上会用到read&write两大系统调用。可能不同操作系统,名称不完全一样,但是功能是一样的。
  • 先强调一个基础知识:read系统调用,并不是把数据直接从物理设备,读数据到内存。write系统调用,也不是直接把数据,写入到物理设备。
  • read系统调用,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。底层的读写交换,是由操作系统kernel内核完成的。

内核缓冲与进程缓冲区:

  • 缓冲区的目的,是为了减少频繁的系统IO调用。大家都知道,系统调用需要保存之前的进程数据和状态等信息,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。
  • 有了缓冲区,操作系统使用read函数把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区中。等待缓冲区达到一定数量的时候,再进行IO的调用,提升性能。至于什么时候读取和存储则由内核来决定,用户程序不需要关心。
  • 在linux系统中,系统内核也有个缓冲区叫做内核缓冲区。每个进程有自己独立的缓冲区,叫做进程缓冲区。
  • 所以,用户程序的IO读写程序,大多数情况下,并没有进行实际的IO操作,而是在读写自己的进程缓冲区。

java IO读写的底层流程:

  • 用户程序进行IO的读写,基本上会用到系统调用read&write,read把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区,它们不等价于数据在内核缓冲区和磁盘之间的交换。在这里插入图片描述
  • 首先看看一个典型Java 服务端处理网络请求的典型过程: (1)客户端请求 Linux通过网卡,读取客户断的请求数据,将数据读取到内核缓冲区。 (2)获取请求数据 服务器从内核缓冲区读取数据到Java进程缓冲区。 (1)服务器端业务处理 Java服务端在自己的用户空间中,处理客户端的请求。 (2)服务器端返回数据 Java服务端已构建好的响应,从用户缓冲区写入系统缓冲区。 (3)发送给客户端 Linux内核通过网络 I/O ,将内核缓冲区中的数据,写入网卡,网卡通过底层的通讯协议,会将数据发送给目标客户端。

阻塞与非阻塞:

  • 阻塞IO,指的是需要内核IO操作彻底完成后,才返回到用户空间,执行用户的操作。
  • 阻塞指的是用户空间程序的执行状态,用户空间程序需等到IO操作彻底完成。
  • 传统的IO模型都是同步阻塞IO。在java中,默认创建的socket都是阻塞的。
  • 非阻塞IO,指的是用户程序不需要等待内核IO操作完成后,内核立即返回给用户一个状态值,用户空间无需等到内核的IO操作彻底完成,可以立即返回用户空间,执行用户的操作,处于非阻塞的状态。
  • 简单的说:阻塞是指用户空间(调用线程)一直在等待,而且别的事情什么都不做;非阻塞是指用户空间(调用线程)拿到状态就返回,IO操作可以干就干,不可以干,就去干的事情。
  • 非阻塞IO要求socket被设置为NONBLOCK。
  • 强调一下,这里所说的NIO(同步非阻塞IO)模型,并非Java的NIO(New IO)库。

同步与异步:

  • 同步IO,是一种用户空间与内核空间的调用发起方式。
  • 同步IO是指用户空间线程是主动发起IO请求的一方,内核空间是被动接受方。
  • 异步IO则反过来,是指内核kernel是主动发起IO请求的一方,用户线程是被动接受方。

服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种::

  • (1)同步阻塞IO(Blocking IO)
  • (2)同步非阻塞IO(Non-blocking IO)
  • (3)IO多路复用(IO Multiplexing) 即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。
  • (4)异步IO(Asynchronous IO) 异步IO,指的是用户空间与内核空间的调用方式反过来。用户空间线程是变成被动接受的,内核空间是主动调用者。这一点,有点类似于Java中比较典型的模式是回调模式,用户空间线程向内核空间注册各种IO事件的回调函数,由内核去主动调用。

同步阻塞IO(Blocking IO):

  • 在linux中的Java进程中,默认情况下所有的socket都是blocking IO。在阻塞式 I/O模型中,应用程序在从IO系统调用开始,一直到到系统调用返回,这段时间是阻塞的。返回成功后,应用进程开始处理用户空间的缓存数据。在这里插入图片描述
  • 举个例子,发起一个blocking socket的read读操作系统调用,流程大概是这样: (1)当用户线程调用了read系统调用,内核(kernel)就开始了IO的第一个阶段:准备数据。很多时候,数据在一开始还没有到达(比如,还没有收到一个完整的Socket数据包),这个时候kernel就要等待足够的数据到来。 (2)当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。 (3)从开始IO读的read系统调用开始,用户线程就进入阻塞状态。一直到kernel返回结果后,用户线程才解除block的状态,重新运行起来。
  • 所以,blocking IO的特点就是在内核进行IO执行的两个阶段,用户线程都被block了。
  • BIO的优点: 程序简单,在阻塞等待数据期间,用户线程挂起。用户线程基本不会占用 CPU 资源。
  • BIO的缺点: 一般情况下,会为每个连接配套一条独立的线程,或者说一条线程维护一个连接成功的IO流的读写。在并发量小的情况下,这个没有什么问题。但是,当在高并发的场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上,BIO模型在高并发场景下是不可用的。

同步非阻塞NIO(None Blocking IO):

  • 在linux系统下,可以通过设置socket使其变为non-blocking。NIO模型中应用程序在一旦开始IO系统调用,会出现以下两种情况: (1)在内核缓冲区没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。 (2)在内核缓冲区有数据的情况下,是阻塞的,直到数据从内核缓冲复制到用户进程缓冲。复制完成后,系统调用返回成功,应用进程开始处理用户空间的缓存数据。在这里插入图片描述

  • 举个例子。发起一个non-blocking socket的read读操作系统调用,流程是这个样子: (1)在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。用户线程需要不断地发起IO系统调用。 (2)内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。 (3)用户线程才解除block的状态,重新运行起来。经过多次的尝试,用户线程终于真正读取到数据,继续执行。

  • NIO的特点: 应用程序的线程需要不断的进行 I/O 系统调用,轮询数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止。

  • NIO的优点: 每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

  • NIO的缺点: 需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。

  • 总之,NIO模型在高并发场景下,也是不可用的。一般 Web 服务器不使用这种 IO模型。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。java的实际开发中,也不会涉及这种IO模型。

  • 再次说明,Java NIO(New IO) 不是IO模型中的NIO模型,而是另外的一种模型,叫做IO多路复用模型( IO multiplexing )。

IO多路复用模型(I/O multiplexing):

  • 如何避免同步非阻塞NIO模型中轮询等待的问题呢?这就是IO多路复用模型。

  • IO多路复用模型,就是通过一种新的系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核kernel能够通知程序进行相应的IO系统调用。

  • 目前支持IO多路复用的系统调用,有select,epoll等等。select系统调用,是目前几乎在所有的操作系统上都有支持,具有良好跨平台特性。epoll是在linux2.6内核中提出的,是select系统调用的linux增强版本。

  • IO多路复用模型的基本原理就是select/epoll系统调用,单个线程不断的轮询select/epoll系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。因此,好处也就显而易见了——通过一次select/epoll系统调用,就查询到到可以读写的一个甚至是成百上千的网络连接。

  • 举个例子。发起一个多路复用IO的的read读操作系统调用,流程是这个样子:在这里插入图片描述

  • 在这种模式中,首先不是进行read系统调动,而是进行select/epoll系统调用。当然,这里有一个前提,需要将目标网络连接,提前注册到select/epoll的可查询socket列表中。然后,才可以开启整个的IO多路复用模型的读流程。 (1)进行select/epoll系统调用,查询可以读的连接。kernel会查询所有select的可查询socket列表,当任何一个socket中的数据准备好了,select就会返回。 当用户进程调用了select,那么整个线程会被block(阻塞掉)。 (2)用户线程获得了目标连接后,发起read系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。 (3)用户线程才解除block的状态,用户线程终于真正读取到数据,继续执行。

  • 多路复用IO的特点: IO多路复用模型,建立在操作系统kernel内核能够提供的多路分离系统调用select/epoll基础之上的。多路复用IO需要用到两个系统调用(system call), 一个select/epoll查询调用,一个是IO的读取调用。 和NIO模型相似,多路复用IO需要轮询。负责select/epoll查询调用的线程,需要不断的进行select/epoll轮询,查找出可以进行IO操作的连接。 另外,多路复用IO模型与前面的NIO模型,是有关系的。对于每一个可以查询的socket,一般都设置成为non-blocking模型。只是这一点,对于用户程序是透明的(不感知)。

  • 多路复用IO的优点: 用select/epoll的优势在于,它可以同时处理成千上万个连接(connection)。与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。 Java的NIO(new IO)技术,使用的就是IO多路复用模型。在linux系统上,使用的是epoll系统调用。

  • 多路复用IO的缺点: 本质上,select/epoll系统调用,属于同步IO,也是阻塞IO。都需要在读写事件就绪后,自己负责进行读写,也就是说这个读写过程是阻塞的。

  • 如何充分的解除线程的阻塞呢?那就是异步IO模型。

异步IO模型(asynchronous IO):

  • 如何进一步提升效率,解除最后一点阻塞呢?这就是异步IO模型,全称asynchronous I/O,简称为AIO。

  • AIO的基本流程是:用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。

  • kernel的数据准备是将数据从网络物理设备(网卡)读取到内核缓冲区;kernel的数据复制是将数据从内核缓冲区拷贝到用户程序空间的缓冲区。在这里插入图片描述

  • 举个例子 (1)当用户线程调用了read系统调用,立刻就可以开始去做其它的事,用户线程不阻塞。 (2)内核(kernel)就开始了IO的第一个阶段:准备数据。当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存)。 (3)kernel会给用户线程发送一个信号(signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。 (4)用户线程读取用户缓冲区的数据,完成后续的业务操作。

  • 异步IO模型的特点: 在内核kernel的等待数据和复制数据的两个阶段,用户线程都不是block(阻塞)的。用户线程需要接受kernel的IO操作完成的事件,或者说注册IO操作完成的回调函数,到操作系统的内核。所以说,异步IO有的时候,也叫做信号驱动 IO 。

  • 异步IO模型缺点: 需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作。

  • 目前来说, Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就目前的业界形式来说,Windows系统,很少作为百万级以上或者说高并发应用的服务器操作系统来使用。

  • 而在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。所以,这也是在 Linux 下,实现高并发网络编程时都是以 IO复用模型模式为主。

小结一下:

  • 四种IO模型,理论上越往后,阻塞越少,效率也是最优。
  • 在这四种 I/O 模型中,前三种属于同步 I/O,因为其中真正的 I/O 操作将阻塞线程。
  • 只有最后一种,才是真正的异步 I/O 模型,可惜目前Linux 操作系统尚欠完善。

同步和异步、阻塞和非阻塞

同步和异步:

  • 同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
  • 而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

阻塞和非阻塞:

  • 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

select 和 epoll底层实现原理

预备知识:

  • 网络编程的核心对象是socket,当创建socket时在底层会创建一个由文件系统管理的socket对象。这个对象包括了发送缓冲区,接收缓冲区,等待队列。
  • recv函数用于从某一个socket中接受流量,但是这个函数在被调用入进程会一直处于阻塞状态,直到从该socket收到数据为止。

网卡接收流量的流程:

  • 步骤一:进程中调用了recv函数请求接收指定socket的流量。
  • 步骤二:操作系统将这个进程加入到对应socket的等待队列中,并从CPU工作队列中移除,经过这一步后,进程会处理阻塞状态。
  • 计算机接收到对端传输的数据,网卡把数据写入到内存中。这一步不需要CPU参与(数据不经过CPU直接从IO设备写入内存的技术叫作DMA技术)
  • 数据写完后网卡会发送一个中断信号,中断CPU,通知CPU有数据到达。
  • CPU中断程序响应中断,并把内存中的数据写入了对应socket的缓冲区里
  • CPU唤醒进程,把进程从socket的等待队列中移除,然后加入到工作队列等待系统调用。

上面的流程有一个问题:

  • revc函数只能监控一个socket,并且会导致进程一直阻塞在这个socket中,直到socket中有数据返回为止。如果有多个socket监控,则需要创建多个进程,非常浪费资源。
  • 而select和epoll就解决了上面的问题,它们让一个进程可以监控多个socket,下面分别说一下两个函数的实现细节。

select的实现细节:

  • select一次监控多个socket的原理很简单
  • 它会把进程加入到它需要监控的所有socket的等待队列中,然后将进程从CPU 工作队列中移除,进入阻塞状态。
  • 当这些socket中有一个socket有数据返回时,中断程序会把进程从所以的socket等待队列中移除,并把进程重新加入到CPU工作队列中,让进程进就绪状态。
  • 进程进入被CPU调用到,只需要遍历所有socket的状态,就可以知道哪些socket可以读取数据了。
  • 操作完这些可读取数据的socket之后,又会重复第一步,把进程加入到所有的 socket中,然后让进程进入阻塞状态。
  • 通过上述的方式,select函数实现了在一个进程中监控多个socket的方法。但是这函数的性能并不高,因为它需要重复把进程从所有的socket中加入/移除。因此它监控的socket数量不能太多,底层规定不能超过1024个。
  • epoll函数针对select的这个缺陷作了改进,接下来说说epoll函数的实现细节。

epoll函数的实现细节:

  • 当进程调用epoll监控多个socket时,会在底层创建一个eventpoll对象,这个对象中包含一个重要的队列:就绪队列
  • 进程调用epoll函数后,epoll会把这个进程加入eventpoll对象的等待队列中
  • 然后把eventpoll对象加入到所有socket的等待队列中,并让CPU阻塞住
  • 当某一个socket有数据返回时,CPU中断程序会把这个socket加入到eventpoll对象的就绪队列中,并把eventpoll中等待的进程唤醒。
  • 进程被唤醒后直接从就绪队列中获取socket读取数据
  • 数据读取完成后,epoll又会把进程加入到eventpoll的等待队列中,然后让CPU阻塞住。
  • epoll针对select优化的点: ①除了第一次外,epoll不需操作所有socket对象的等待队列,只需要操作eventpoll的等待队列即可 ②进程被唤醒后,不需要遍历即可直接知道哪些socket准备好了。

总结:

  • (1)进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select;这样select/poll可以帮我们侦测许多fd是否就绪;但是select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限。linux还提供了一个epoll系统调用,epoll是基于事件驱动方式,而不是顺序扫描,当有fd就绪时,立即回调函数rollback。
  • (2)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
  • (3)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
  • BIO\NIO\AIO再理解,nio轮询方式与epoll
  • bio,nio,aio的区别和select,poll,epoll的区别

零拷贝,用户空间和内核空间数据传递,mmap

简介:

  • redis-netty-Kafka底层都用epoll()函数实现。

零拷贝: 面试被问到“零拷贝”!你真的理解吗? Linux中的零拷贝

用户空间和内核空间数据传递: linux 用户空间和内核空间数据传递

mmap: mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。 关于mmap的解析

网络IO与文件IO

简介: 在这里插入图片描述

文件IO:

  • java.io.File是用于操作文件或目录的类
  • 字符流 Java使用流来读写文件, 字符流用来读写文本文件. 所有的字符流类都在java.io包中.
  • 字节流 字节流用于读写二进制文件, 其读写的数据以byte[]类型存储. 所有字节流类都在java.io包中.
  • 标准输入输出 java.lang.System对象中维护了3个标准流, 用于终端输入输出: System.out, 标准输出流, PrintStream对象 System.err: 标准错误流, PrintStream对象 System.in: 标准输入流, FileInputStream对象

网络IO:

  • TCP客户端 java.net.Socket是一个用作Tcp客户端的Socket. 从Socket中获得InputStream和OutputStream对象就可以与服务器通过Tcp连接通信了.

  • TCP服务端 java.net.ServerSocket则是一个用作Tcp服务端的socket.

  • UDP客户端 java.net.Datagram可以用作UDP客户端, 其接收到的数据报被封装为java.net.DatagramPacket.

非阻塞IO通道:

  • 无文件IO

在这里插入图片描述 在这里插入图片描述 阻塞IO通道: 在这里插入图片描述

NIO简介

简介: Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IOAPI,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。

IO与NIO的区别:

IONIO
面向流(Stream Oriented)面向缓冲区(Buffer Oriented)
阻塞IO(Blocking IO)非阻塞IO(Non Blocking IO)
(无)选择器(Selectors)

缓冲区(Buffer)和通道(Channel)

简介:

  • Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
  • 简而言之,Channel 负责传输, Buffer 负责存储。

缓冲区

缓冲区(Buffer):

  • 缓冲区(Buffer):一个用于特定基本数据类 型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。
  • Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
  • Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。 - Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类: ByteBuffer、 CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、 FloatBuffer、DoubleBuffer
  • 上述 Buffer 类 他们都采用相似的方法进行管理数据,只是各自 管理的数据类型不同而已。都是通过如下方法获取一个 Buffer 对象: static XxxBuffer allocate(int capacity) : 创建一个容量为 capacity 的XxxBuffer 对象

缓冲区的基本属性:

  • 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创 建后不能更改。
  • 限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
  • 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为 负,并且不能大于其限制
  • 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的position,之后可以通过调用 reset() 方法恢复到这 个 position.
  • 标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity

缓冲区的数据操作:

  • Buffer 所有子类提供了两个用于数据操作的方法:get() 与 put() 方法
  • 获取 Buffer 中的数据 get() :读取单个字节 get(byte[] dst):批量读取多个字节到 dst 中 get(int index):读取指定索引位置的字节(不会移动 position)
  • 放入数据到 Buffer 中 put(byte b):将给定单个字节写入缓冲区的当前位置 put(byte[] src):将 src 中的字节写入缓冲区的当前位置 put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)
  • 其他方法在这里插入图片描述

直接与非直接缓冲区:

  • 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在 此缓冲区上执行本机 I/O操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
  • 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的 本机 I/O操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好 处时分配它们。
  • 直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
  • 字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。

直接与非直接缓冲区图示:

  • 非直接缓冲区:在这里插入图片描述
  • 直接缓冲区:在这里插入图片描述

通道

简介:

  • 通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。

通道图示:

  • 直接通过CPU控制:在这里插入图片描述

  • 通过DMA控制:在这里插入图片描述

  • 通过通道控制:在这里插入图片描述

Java 为 Channel 接口提供的最主要实现类如下:

  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • DatagramChannel:通过 UDP 读写网络中的数据通道。
  • SocketChannel:通过 TCP 读写网络中的数据。
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。

获取通道:

  • 获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下: FileInputStream、FileOutputStream、RandomAccessFile、DatagramSocket、Socket、ServerSocket、
  • 获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。
  • 或者通过通道的静态方法 open()打开并返回指定通道。

分散(Scatter)和聚集(Gather):

  • 分散读取(Scattering Reads)是指从 Channel 中读取的数据“分散”到多个 Buffer 中。在这里插入图片描述注意:按照缓冲区的顺序,从 Channel 中读取的数据依次将 Buffer 填满。
  • 聚集写入(Gathering Writes)是指将多个 Buffer 中的数据“聚集” 到 Channel。在这里插入图片描述注意:按照缓冲区的顺序,写入 position 和 limit 之间的数据到 Channel 。

通道之间传输:

  • 将数据从源通道传输到其他 Channel 中:transferFrom()
  • 将数据从源通道传输到其他 Channel 中:transferTo()

FileChannel 的常用方法: 在这里插入图片描述

FileChannel

简述:

  • Channel是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),

  • 而且通道可以用于读、写或者同事用于读写。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

  • NIO中通过channel封装了对数据源的操作,通过channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。

  • 这个数据源可能是多种的。比如,可以是文件,也可以是网络socket。在大多数应用中,channel与文件描述符或者socket是一一对应的。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。

注意:

  • 文件通道总是阻塞式的,因此不能被置于非阻塞模式。现代操作系统都有复杂的缓存和预取机制,使得本地磁盘I/O操作延迟很少。网络文件系统一般而言延迟会多些,不过却也因该优化而受益。面向流的I/O的非阻塞范例对于面向文件的操作并无多大意义,这是由文件I/O本质上的不同性质造成的。对于文件I/O,最强大之处在于异步I/O(asynchronousI/O),它允许一个进程可以从操作系统请求一个或多个I/O操作而不必等待这些操作的完成。发起请求的进程之后会收到它请求的I/O操作已完成的通知。
  • FileChannel对象是线程安全(thread-safe)的。多个进程可以在同一个实例上并发调用方法而不会引起任何问题,不过并非所有的操作都是多线程的(multithreaded)。影响通道位置或者影响文件大小的操作都是单线程的(single-threaded)。如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。并发行为也会受到底层的操作系统或文件系统影响。
  • 每个FileChannel对象都同一个文件描述符(file descriptor)有一对一的关系,所以上面列出的API方法与在您最喜欢的POSIX(可移植操作系统接口)兼容的操作系统上的常用文件I/O系统调用紧密对应也就不足为怪了。本质上讲,RandomAccessFile类提供的是同样的抽象内容。在通道出现之前,底层的文件操作都是通过RandomAccessFile类的方法来实现的。FileChannel模拟同样的I/O服务,因此它的API自然也是很相似的。
  • Channel通道介绍及FileChannel详解

NIO 的非阻塞式网络通信

阻塞与非阻塞:

  • 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不 能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理, 当服务器端需要处理大量客户端时,性能急剧下降。
  • Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入 和输出通道。因此,NIO可以让服务器端使用一个或有限几个线程来同 时处理连接到服务器端的所有客户端。

选择器(Selector)

简介:

  • NIO有选择器,而IO没有。
  • 选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可 以同时监控多个SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个Channel。Selector 是非阻塞 IO 的核心。
  • SelectableChannle 的结构如下图:在这里插入图片描述

选择器(Selector)的应用:

  • 创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
  • 向选择器注册通道:SelectableChannel.register(Selector sel, int ops)。
  • 当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
  • 可以监听的事件类型(可使用 SelectionKey 的四个常量表示): 读 : SelectionKey.OP_READ (1) 写 : SelectionKey.OP_WRITE (4) 连接 : SelectionKey.OP_CONNECT (8) 接收 : SelectionKey.OP_ACCEPT (16)
  • 若注册时不止监听一个事件,则可以使用“位或”操作符连接。
  • SelectionKey:表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整 数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操 作。

Selector 的常用方法: 在这里插入图片描述在这里插入图片描述

管道 (Pipe)

简介:

  • Java NIO管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。在这里插入图片描述