深入浅出理解Java NIO系列—第1篇

深入浅出理解Java NIO系列—第1篇 概述

1. IO和NIO的含义

1.1 IO的含义

    讲NIO之前,我们先来看一下IO。Java IO即Java 输入输出系统。不管我们编写何种应用,都难免和各种输入输出相关的媒介打交道,其实和媒介进行IO的过程是十分复杂的,这要考虑的因素特别多,比如我们要考虑和哪种媒介进行IO(文件、控制台、网络),我们还要考虑具体和它们的通信方式(顺序、随机、二进制、按字符、按字、按行等等)。Java类库的设计者通过设计大量的类来攻克这些难题,这些类就位于java.io包中。

    由于老的Java IO标准类提供IO操作(如read(),write())都是同步阻塞的,因此,IO通常也被称为阻塞IO(即BIO,Blocking I/O)。

1.2 NIO含义

    在JDK1.4之后,为了提高Java IO的效率,Java又提供了一套新的IO(NIO,New I/O),原因在于它相对于之前的I/O类库是新增的。由于之前老的I/O类库是阻塞I/O,New I/O类库的目标就是要让Java支持非阻塞I/O,所以,更多的人喜欢称之为非阻塞I/O(Non-block I/O)。


2. 阻塞与非阻塞

2.1 四种I/O模型

  • 同步阻塞 IO :   

    在此种方式下,用户进程在发起一个 IO 操作以后,必须等待 IO 操作的完成,只有当真正完成了 IO 操作以后,用户进程才能运行。 JAVA传统的 IO 模型属于此种方式!    

  • 同步非阻塞 IO:

    在此种方式下,用户进程发起一个 IO 操作以后 便可返回做其它事情,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。其中目前 JAVA 的 NIO 就属于同步非阻塞 IO 。    

  • 异步阻塞 IO :  

    此种方式下是指应用发起一个 IO 操作以后,不等待内核 IO 操作的完成,等内核完成 IO 操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问 IO 是否完成,那么为什么说是阻塞的呢?因为此时是通过 select 系统调用来完成的,而 select 函数本身的实现方式是阻塞的,而采用 select 函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!   

  • 异步非阻塞 IO:   

    在此种模式下,用户进程只需要发起一个 IO 操作然后立即返回,等 IO 操作真正的完成以后,应用程序会得到 IO 操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的 IO 读写操作,因为 真正的 IO读取或者写入操作已经由 内核完成了。目前 Java 中还没有支持此种 IO 模型。

2.2 阻塞I/O

    所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。Java IO的各种流是阻塞的。这意味着当线程调用write()或read()时,线程会被阻塞,直到有一些数据可用于读取或数据被完全写入。

    需要说明的是等待就绪引起的“阻塞”是不使用CPU的,是在“空等”;而真正的读写操作引起的“阻塞”是使用CPU的,是真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。因此,所谓“阻塞”主要是指等待就绪的过程。

以socket.read()为例子:

    传统的阻塞IO(BIO)里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。

    而对于非阻塞IO(NIO),如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。

    换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了"。NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。

2.3 非阻塞I/O

     Java NIO非阻塞模式允许线程请求向通道写入数据,但不等待它被完全写入,允许线程继续进行,并做其他事情。


3. 面向流与面向缓冲

3.1 面向流

    Java IO是面向流的I/O,这意味着我们需要从流中读取一个或多个字节。它使用流来在数据源/槽和java程序之间传输数据。使用此方法的I/O操作较慢。
    下面来看看在Java程序中使用输入/输出流的数据流图(注意:图中输入|输出均以Java Program为参照物):

深入浅出理解Java NIO系列—第1篇_第1张图片

3.2 面向缓冲

    Java NIO是面向缓存的I/O方法。 将数据读入缓冲器,再使用通道进一步处理数据。 在NIO中,使用通道和缓冲区来处理I/O操作。
    通道和流之间的主要区别是:
  • 流可以用于单向数据传输。
  • 通道提供双向数据传输。
    因此,通过在java NIO中引入通道,可以执行非阻塞I/O操作。
    如下图所示:通道,缓冲区,java程序,数据源和数据接收器之间的相互作用 。

深入浅出理解Java NIO系列—第1篇_第2张图片


4 通道(Channels)

    在Java NIO中,通道是在实体和字节缓冲区之间有效传输数据的媒介。 它从一个实体读取数据,并将其放在缓冲区块中以供消费。通道作为Java NIO提供的网关来访问I/O机制。通常,通道与操作系统文件描述符具有一对一关系,用于提供平台独立操作功能。

NIO通道基础

    通道使用本地代码执行实际工作。通道接口允许我们以便携和受控的方式访问低级I/O服务。在层次结构的顶部,通道接口如下所示:
package java.nio.channels;  
 public interface Channel{  
    public boolean isOpen();  
    public void close() throws IOException;  
}
    正如在上述通道接口中看到的,所有通道中有常见的两个操作:
  • 检查通道是否关闭(isOpen)
  • 关闭通道(close)


5 选择器(Selectors)

    在Java NIO中,选择器是可选择通道的多路复用器,可用作可以进入非阻塞模式的特殊类型的通道。它可以检查一个或多个NIO通道,并确定哪个通道准备好进行通信,即读取或写入。

5.1 选择器(Selectors)的用途是什么?

    选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。下面来看看使用选择器来处理3个通道的线程的示意图:

深入浅出理解Java NIO系列—第1篇_第3张图片

创建选择器

    可以通过调用Selector.open()方法创建一个选择器,如下代码所示:

Selector selector = Selector.open();

以下将以socket编程为例,图文并茂浅析NIO机制中Selector的作用。

5.2 Java传统I/O在socket编程中的应用场景,

深入浅出理解Java NIO系列—第1篇_第4张图片

    一个socket连接对应一个输入/输出通道,一个输入/输出通道在对应一个线程,这样有多个请求,就需要开辟多个线程,这样对CPU的负担大大的提高。关系是一对一的。

5.3 Java NIO 在socket编程中的应用场景

深入浅出理解Java NIO系列—第1篇_第5张图片

在上图中我们能看到中间有类叫做Selector 

  1. 当socket服务端启动后对每个socketchannel进行建立关系。
  2. 这时只需要把探知的socketchannel告诉Selector。
  3. 当有事件发生时,会通知传回一组SelectionKey,读取这些Key,就会获得我们刚刚注册过的socketchannel。
  4. 原先已经建立好的Channel中读取数据。
  5. 最后可以接收到的这些数据进行处理。

    所以我们能看出不必像原先那样开大量的线程,然后让线程傻傻的等待。而NIO不必,通道已经由一个线程建立好了,有事就说话,说了就去做,而且在一个线程中建立了多个通道,可以同时做很多事情,不会像原先那样开辟多个线程,耗费CPU资源,关系是1对多的。


6. IO与NIO的主要区别

6.1 工作方式不同 

IO是面向流的,NIO是面向缓冲区的

  • Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方;
  • NIO则能前后移动流中的数据,因为是面向缓冲区的;
IO流是阻塞的,NIO流是不阻塞的

  • Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
  • Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。 
  • 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

选择器

  • Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。 

6.2 使用场景不同

NIO

  • 优势在于一个线程管理多个通道;但是数据的处理将会变得复杂;
  • 如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,采用这种;
传统的IO
  • 适用于一个线程管理一个通道的情况;因为其中的流数据的读取是阻塞的;
  • 如果需要管理同时打开不太多的连接,这些连接会发送大量的数据;


参考文献:

http://ju.outofmemory.cn/entry/6835

https://www.yiibai.com/java_nio/java-nio-vs-input-output.html


你可能感兴趣的:(Java)