Java NIO解析

翻译自:http://tutorials.jenkov.com/java-nio/index.html

1. 什么是Java NIO

Java NIO(New IO)是用于Java(来自Java 1.4)的替代IO API,意味着替代标准Java IO和Java Networking API。 Java NIO提供了与标准IO API不同的IO工作方式。

1.1 Java NIO: Channels 和 Buffers

在标准IO API中,您可以使用字节流和字符流。在NIO中,您使用通道(channel)和缓冲区(buffer)。数据总是从通道读入缓冲区,或从缓冲区写入通道。

1.2 Java NIO: 非阻塞(Non-blocking) IO

Java NIO使您可以执行非阻塞IO。例如,一个线程可以请求一个channel将数据读入缓冲区buffer。channel读取数据到buffer时,线程可以做其他事情。一旦数据被读入buffer,线程就可以继续处理它。将数据写入channel也是如此。

1.3 Java NIO: Selectors

Java NIO包含“选择器(selectors)”的概念。selectors是一个可以监视多个channel事件的对象(如:连接打开,数据到达等)。因此,一个线程可以监视多个通道的数据。


2.Java NIO 概述

Java NIO由以下核心组件组成:

  • Channels
  • Buffers
  • Selectors

Java NIO拥有比这些更多的类和组件,但在我看来,Channel,Buffer 和Selector构成了API的核心。 其他组件,如Pipe和FileLock,仅仅是与三个核心组件结合使用的工具类。 因此,我将在本文中关注这三个组件。

2.1 Channels 和 Buffers

通常,NIO中的所有IO都以一个 Channel 开始。 Channel 有点像流(stream)。 Channel数据可以读入Buffer。 数据也可以从Buffer写入Channel。 这是一个例子:


这里有几种ChannelBuffer类型。 以下是Java NIO中Channel的4种主要实现:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

如您所见,这些通道涵盖UDP + TCP网络IO和文件IO。

这些类也有一些有趣的接口,但为了简单起见,我将使它们远离Java NIO概述。

以下是Java NIO中的核心Buffer实现:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
这些Buffer覆盖了可以通过IO发送的基本数据类型:byte,short,int,long,float,double和characters。

Java NIO也有一个与内存映射文件结合使用的MappedByteBuffer。 尽管如此,我会将此Buffer从此概览中移出。

2.2 Selectors

选择器(Selector)允许单个线程处理多个Channel。 如果您的应用程序有许多连接(Channel)打开,但每个连接只有低流量,这很方便。 例如,在聊天服务器中。

下面是一个使用Selector处理3个Channel的线程的例子:


要使用Selector,您需要注册Channel。 然后你调用它的select()方法。 此方法将阻塞,直到有一个事件准备好注册的Channel之一。 一旦该方法返回,该线程就可以处理这些事件。 事件的例子是传入连接,数据收到等。

3. Java NIO Channel

Java NIO Channel类似于与 Stream 相似却有一点差异:

     您可以读取和写入ChannelStream通常是单向的(读或写)。
     Channel可以被异步读取和写入。
     Channel始终读取或写入缓冲区。

如上所述,您将Channel中的数据读入Buffer,并将数据从Buffer写入Channel。 这是一个例子:


3.1 Channel 实现

以下是Java NIO中最重要的Channel实现:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel
FileChannel从文件读取数据。

DatagramChannel可以通过UDP在网络上读取和写入数据。

SocketChannel可以通过TCP在网络上读写数据。

ServerSocketChannel允许您监听传入的TCP连接,就像Web服务器一样。 为每个传入连接创建一个SocketChannel。


3.2 基本 Channel 实例

这是一个基本的例子,它使用FileChannel将一些数据读入Buffer缓冲区:

    RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
    FileChannel inChannel = aFile.getChannel();

    ByteBuffer buf = ByteBuffer.allocate(48);

    int bytesRead = inChannel.read(buf);
    while (bytesRead != -1) {

      System.out.println("Read " + bytesRead);
      buf.flip();

      while(buf.hasRemaining()){
          System.out.print((char) buf.get());
      }

      buf.clear();
      bytesRead = inChannel.read(buf);
    }
    aFile.close()

注意buf.flip()调用。 首先你读入一个Buffer。 然后你翻转它并且读出来。


4. Java NIO Buffer

与NIO Channel交互时使用Java NIO Buffer如您所知,数据从Channel读入Buffer,并从Buffer写入Channel

Buffer本质上是一块内存,您可以在其中写入数据,然后您可以再次读取数据。该内存块被封装在一个NIO缓冲区对象中,该对象提供了一组方法,可以更轻松地使用内存块。

4.1 基本缓冲区Buffer使用


使用Buffer读取和写入数据通常遵循以下四个步骤:

    
将数据写入Buffer
    
调用buffer.flip()
    
Buffer读取数据
    
调用buffer.clear()或buffer.compact()

将数据写入Buffer时,Buffer会跟踪您写入的数据量。一旦您需要读取数据,您需要使用flip()方法调用将Buffer从写入模式切换到读取模式。在读取模式下,Buffer可让您读取写入缓冲区的所有数据。

读完所有数据后,您需要清除Buffer,使其准备好再次写入。你可以通过两种方式来做到这一点:通过调用clear()或通过调用compact()。 clear()方法清除整个缓冲区。 compact()方法只会清除已读取的数据。任何未读数据将被移动到Buffer的开始处,并且数据将在未读数据之后写入Buffer

下面是一个简单的Buffer使用示例,其中写入,翻转,读取和清除操作以粗体显示:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();

4.2 Buffer容量,位置和限制

缓冲区本质上是一块内存,您可以在其中写入数据,然后您可以再次读取数据。 该内存块被封装在一个NIO缓冲区对象中,该对象提供了一组方法,可以更轻松地使用内存块。

缓冲区有三个你需要熟悉的属性,以便了解缓冲区的工作原理。 这些是:

     容量
     位置
     限制

位置和限制的含义取决于缓冲区是处于读取还是写入模式。 无论缓冲模式如何,容量总是意味着相同。

以下是写入和读取模式下容量,位置和限制的说明。 解释如下图所示。
分别在Write 和Read 模式下的 Buffer

容量
作为一个内存块,Buffer具有一定的大小,也称为“容量”。您只能将容量字节,长整数,字符等写入缓冲区。一旦缓冲区已满,您需要将其清空(读取数据或清除数据),然后才能向其中写入更多数据。


位置
当您将数据写入缓冲区时,您可以在某个位置执行此操作。最初的位置为0.当一个字节,长字符等被写入缓冲区时,位置会前进到指向缓冲区中的下一个单元以插入数据。职位可以最大限度地成为能力 - 1。

当你从缓冲区读取数据时,你也可以从给定的位置进行读取。当您将缓冲区从写入模式翻转到读取模式时,位置将重置为0.当您从缓冲区中读取数据时,您可以从位置读取数据,并且位置会前进到下一个读取位置。

限制

在写模式下,Buffer的限制是可以写入Buffer的数据量的限制,限制等于Buffer的容量。

当将Buffer转换为读取模式时,限制意味着您可以从数据中读取多少数据的限制。因此,当将Buffer转换为读取模式时,将限制设置为写入模式的写入位置。换句话说,您可以读取与写入数量相同的字节数(限制设置为写入的字节数,由位置标记)。

4.3 Buffer的种类

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

如您所见,这些Buffer类型表示不同的数据类型。 换句话说,它们允许您使用char,short,int,long,float或double来处理缓冲区中的字节。

MappedByteBuffer有点特别,并且将在其自己的文本中进行介绍。


5.Java NIO Selector

Selector是一个Java NIO组件,它可以检查一个或多个NIO Channel,并确定哪些Channel已准备好,例如, Reading或Writing。 这样一个线程可以管理多个Channel,从而可以管理多个网络连接。

5.1 为什么使用NIO Selector?

使用单个线程处理多个Channel的优点是您需要较少的线程来处理Channel。(Tomcat这类web容器就运用了这样的原理) 实际上,您只能使用一个线程来处理您的所有Channel。 线程之间的切换对于操作系统而言是昂贵的,并且每个线程也占用操作系统中的一些资源(存储器)。 因此,你使用的线程越少越好。

请记住,现代操作系统和CPU在多任务处理方面越来越好,所以多线程的开销随着时间的推移变得越来越小。 事实上,如果一个CPU有多个内核,那么你可能会因为没有多任务而浪费CPU的功率。 无论如何,该设计讨论属于不同的文本。 在这里说一下就足够了,你可以使用一个Selector来处理单个线程的多个Channel


下面是一个使用Selector处理3个Channel的线程的例子:

5.2 Creating a Selector

通过调用Selector.open()方法创建一个Selector,如下所示:
Selector selector = Selector.open();

5.3 用Selector注册Channel

为了使用具有SelectorChannel,您必须使用Selector注册Channel。 这是使用SelectableChannel.register()方法完成的,如下所示:

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Channel必须处于非阻塞模式才能与Selector一起使用。 这意味着你不能使用FileChannel和Selector,因为FileChannel不能切换到非阻塞模式。 套接字(Socket) channel将正常工作。

注意register()方法的第二个参数。 这是一个“兴趣集”,意味着您希望通过SelectorChannel中收听哪些事件。 您可以听四种不同的活动:

  1. Connect
  2. Accept
  3. Read
  4. Write
“发生事件”的Channel也被认为是“ready”的事件。 所以,成功连接到另一台服务器的Channel是“connect ready”。 接受传入连接的服务器Socket channel是“accept”。 准备好读取数据的Channel是“Read”。 准备好向您写入数据的Channel是“write”。

这四个事件由四个SelectionKey常量表示:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

...

6. Non-blocking Server

6.1 非阻塞IO管道(PipeLines)


非阻塞IO管道是处理非阻塞IO的组件链。这包括以非阻塞方式读取和写入IO。下面是一个简化的非阻塞IO管道的说明:




组件使用Selector来检查Channel何时有数据要读取。然后组件读取输入数据并根据输入生成一些输出。输出再次写入Channel

非阻塞IO管道不需要读取和写入数据。有些流水线只能读取数据,有些流水线只能写数据。

上图仅显示单个组件。非阻塞IO管道可能有多个组件处理传入数据。非阻塞IO管道的长度取决于管道需要做什么。

非阻塞IO管道也可能同时从多个通道读取数据。例如,从多个SocketChannel读取数据。

上图中的控制流程也被简化了。它是通过选择器启动从通道读取数据的组件。不是Channel将数据推入Selector并从那里进入组件,即使这是上图所示。

6.2 非阻塞与阻塞IO管道


非阻塞和阻塞IO管道之间的最大区别在于如何从底层Channel(Socket或File)读取数据。

IO管道通常从某个流Stream(Socket或File)读取数据,并将该数据分解为一致的messages。我将其称为:Message Reader下面是一个Message Reader将stream分解成message的图示:




阻塞IO管道可以使用类似于InputStream的接口,其中一次一个字节可以从底层通道读取,并且类似InputStream的接口阻塞,直到有数据准备好读取。

阻塞IO管道缺点


虽然阻塞Message Reader更容易实现,但它有一个不幸的缺点,即需要为每个Stream分别创建一个线程。这是因为每个Stream的IO接口会发生阻塞,直到有数据可以从中读取。这意味着单个线程无法尝试从一个流读取数据,如果没有数据,则从另一个流读取数据。只要线程试图从流中读取数据,线程就会阻塞,直到实际上有一些数据要读取。

如果IO管道是必须处理大量并发连接的服务器的一部分,那么服务器将需要每个活动输入连接一个线程。如果服务器在任何时候只有几百个并发连接,这可能不成问题。但是,如果服务器具有数百万并发连接,则此类设计不能很好地扩展。每个线程将为其堆栈提供320K(32位JVM)和1024K(64位JVM)内存。因此,1.000.000个线程将占用1TB内存!

为了减少线程数量,许多服务器使用一种设计,在该设计中服务器保持一个线程池(例如100个),该线程池一次从入站连接(inbound connections)读取messages。入站连接保留在队列中,并且线程按顺序处理来自每个入站连接的messages,并将入站连接放入队列中。这里说明这个设计:

但是,这种设计要求入站连接经常地发送数据。 如果入站连接可能处于非活动状态的时间较长,大量的非活动连接实际上可能会阻塞线程池中的所有线程。 这意味着服务器响应缓慢甚至无响应。

一些服务器设计试图通过在线程池中的线程数量具有一定弹性来缓解这个问题。 例如,如果线程池用完线程,则线程池可能启动更多线程来处理负载。 此解决方案意味着需要更多数量的慢速连接才能使服务器无响应。 但请记住,您可以运行多少个线程的上限仍然存在。 所以,这将不能很好地扩展当存在1.000.000个慢速连接。

6.3 基本的非阻塞IO管道设计

非阻塞IO管道可以使用单个线程来读取来自多个Stream的message。 这要求Stream可以切换到非阻塞模式。 处于非阻塞模式时,当您尝试从中读取数据时,Stream可能会返回0个或更多字节。 如果Stream没有要读取的数据,则返回0字节。 当Stream实际上有一些要读取的数据时,返回+1个字节。

为了避免检查有0字节的流,我们使用Java NIO Selector。 一个或多个SelectableChannel实例可以使用Selector进行注册。 当你在Selector上调用select()或selectNow()时,它只给你实际上有数据读取的SelectableChannel实例。 这里说明这个设计:

7. Java NIO VS IO

在研究Java NIO和IO API时,很快就会想到一个问题:

什么时候应该使用IO,什么时候应该使用NIO?

在本文中,我将尝试阐明Java NIO和IO之间的区别,它们的用例以及它们如何影响代码的设计。
主要差异Betwen Java NIO和IO

下表总结了Java NIO和IO之间的主要区别。

IONIO
面向Stream面向 Buffer
阻塞IO非阻塞IO
 Selectors

Java NIO和IO之间的第一大区别是IO是面向流的,其中NIO是面向缓冲区的。那么,这是什么意思?

面向流的Java IO意味着您一次从Stream中读取一个或多个字节。读取字节所做的工作由您决定。它们没有被缓存到任何地方。此外,您无法前后移动数据流中的数据。如果您需要前后移动从Stream中读取的数据,则需要先将其缓存在缓冲区中。

Java NIO的面向缓冲区的方法略有不同。数据被读入缓冲区,稍后进行处理。您可以根据需要前后移动缓冲区。这在处理过程中给你更多的灵活性。但是,您还需要检查缓冲区是否包含您需要的所有数据,以便对其进行全面处理。而且,您需要确保在将更多数据读入缓冲区时,不会覆盖尚未处理的缓冲区中的数据。

阻塞与非阻塞IO

Java IO的各种流都被阻塞。这意味着,当线程调用read()或write()时,该线程被阻塞,直到有一些数据要读取,或数据完全写入。在此期间,该线程无法做其他任何事情。

Java NIO的非阻塞模式使线程能够请求从一个通道读取数据,并且只获取当前可用的数据,或者根本没有任何数据可用。在数据可用于阅读之前,线程可以继续使用其他内容,而不是保持阻塞状态。

无阻塞写入也是如此。一个线程可以请求将一些数据写入一个通道,但不要等待它完全写入。然后,线程可以继续并在此期间做其他事情。

当IO调用中没有被阻塞时,哪些线程花费空闲时间,通常是在其他通道上同时执行IO。也就是说,一个线程现在可以管理多个输入和输出通道。

Selectors

Java NIO的Selector允许单个线程监视多个输入Channel。您可以使用选择器注册多个Channel,然后使用单个线程“选择”可用于处理的输入的Channel,或选择准备写入的Channel这种选择器机制可以让单个线程轻松管理多个Channel


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值