乐趣区

少啰嗦一分钟带你读懂Java的NIO和经典IO的区别

1、引言

很多初涉网络编程的程序员,在研究 Java NIO(即异步 IO)和经典 IO(也就是常说的阻塞式 IO)的 API 时,很快就会发现一个问题:我什么时候应该使用经典 IO,什么时候应该使用 NIO?

在本文中,将尝试用简明扼要的文字,阐明 Java NIO 和经典 IO 之间的差异、典型用例,以及这些差异如何影响我们的网络编程或数据传输代码的设计和实现的。

本文没有复杂理论,也没有像网上基它文章一样千篇一律的复制粘贴,有的只是接地气的通俗易懂,希望能给你带来帮助。

(本文同步发布于:http://www.52im.net/thread-26…)

2、相关文章

《Java 新一代网络编程模型 AIO 原理及 Linux 系统 AIO 介绍》
《Java NIO 基础视频教程、MINA 视频教程、Netty 快速入门视频》

3、Java NIO 和 IO 的主要区别

下表总结了 Java NIO 和 IO 之间的主要区别。我将在表格后面的部分中详细介绍每个区别。

3.1 Stream Oriented vs. Buffer Oriented
Java NIO 和 IO 之间的第一个重要区别是 IO 是面向流的,其中 NIO 是面向缓冲区的。那么,这意味着什么?

面向流的 Java IO 意味着您可以从流中一次读取一个或多个字节。你对读取的字节做什么取决于你。它们不会缓存在任何地方。此外,您无法在流中的数据中前后移动。如果需要在从流中读取的数据中前后移动,则需要先将其缓存在缓冲区中。

Java NIO 的面向缓冲区的方法略有不同。数据被读入缓冲区,稍后处理该缓冲区。你可以根据需要在缓冲区中前后移动。这使你在处理过程中具有更大的灵活性。但是,你还需要检查缓冲区是否包含完整处理所需的所有数据。并且,你需要确保在将更多数据读入缓冲区时,不要覆盖尚未处理的缓冲区中的数据。

3.2 Blocking vs. Non-blocking IO
Java IO 的各种流都是 blocking 的。这意味着,当线程调用 read()或 write()时,该线程将被阻塞,直到有一些数据要读取,或者数据被完全写入,在此期间,该线程无法执行任何其他操作。

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

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

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

4、Selectors

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

5、NIO 和经典 IO 如何影响应用程序的设计?

选择 NIO 或 IO 作为 IO 工具包可能会影响应用程序设计的以下方面:

1)API 调用 NIO 或 IO 类;

2)处理数据;

3)用于处理数据的线程数。

5.1 API 调用
当然,使用 NIO 时的 API 调用看起来与使用 IO 时不同。这并不奇怪。而不是仅仅从例如 InputStream 读取字节的数据字节,必须首先将数据读入缓冲区,然后从那里进行处理。

5.2 数据处理
使用纯 NIO 设计与 IO 设计时,数据处理也会受到影响。

在 IO 设计中,您从 InputStream 或 Reader 中读取字节的数据字节。想象一下,您正在处理基于行的文本数据流。

例如:

Name: Anna

Age: 25

Email: [url=mailto:anna@mailserver.com]anna@mailserver.com[/url]

Phone: 1234567890

这个文本行流可以像这样处理:

InputStream input = ... ; // get the InputStream from the client socket

BufferedReader reader = newBufferedReader(newInputStreamReader(input));


String nameLine   = reader.readLine();

String ageLine    = reader.readLine();

String emailLine  = reader.readLine();

String phoneLine  = reader.readLine();

注意处理状态是如何,由程序执行的程度决定的。换句话说,一旦第一个 reader.readLine()方法返回,您就确定已经读取了整行文本。readLine()会阻塞直到读取整行,这就是原因。您还知道此行包含名称。同样,当第二个 readLine()调用返回时,您知道此行包含年龄等。

正如您所看到的,只有当有新数据要读取时,程序才会进行,并且对于每个步骤,您都知道该数据是什么。一旦执行的线程已经超过读取代码中的某个数据片段,该线程就不会在数据中向后移动(通常不会)。

此图中还说明了此原则:

▲ Java IO:从阻塞流中读取数据

NIO 的实现看起来会有所不同,这是一个简化的例子:

ByteBuffer buffer = ByteBuffer.allocate(48);

intbytesRead = inChannel.read(buffer);

注意第二行从通道读取字节到 ByteBuffer。当该方法调用返回时,您不知道所需的所有数据是否都在缓冲区内。你只知道缓冲区包含一些字节,这使得处理更加困难。

想象一下,在第一次读取(缓冲)调用之后,是否所有读入缓冲区的内容都是半行。例如,“姓名:An”。你能处理这些数据吗?并不是的。在完成任何数据的处理之前,您需要等待至少一整行数据进入缓冲区。

那么你怎么知道缓冲区是否包含足够的数据来处理它?好吧,你没有。找出的唯一方法是查看缓冲区中的数据。结果是,在您知道所有数据是否存在之前,您可能需要多次检查缓冲区中的数据。这既低效又可能在程序设计方面变得混乱。

例如:

ByteBuffer buffer = ByteBuffer.allocate(48);

intbytesRead = inChannel.read(buffer);

while(! bufferFull(bytesRead) ) {

bytesRead = inChannel.read(buffer);

}

bufferFull()方法必须跟踪读入缓冲区的数据量,并返回 true 或 false,具体取决于缓冲区是否已满。换句话说,如果缓冲区已准备好进行处理,则认为它已满。

bufferFull()方法扫描缓冲区,但必须使缓冲区保持与调用 bufferFull()方法之前相同的状态。如果不是,则可能无法在正确的位置读入读入缓冲区的下一个数据。这不是不可能的,但这是另一个需要注意的问题。

如果缓冲区已满,则可以对其进行处理。如果它不满,您可能能够部分处理那里的任何数据,如果这在您的特定情况下是有意义的。在许多情况下,它没有。

这个图中说明了 is-data-in-buffer-ready 循环:

▲ Java NIO:从通道读取数据,直到所有需要的数据都在缓冲区中

6、什么时候该用 NIO?什么时候该用经典 IO?

NIO 允许您仅使用一个(或几个)线程来管理多个通道(网络连接或文件),但成本是解析数据可能比从阻塞流中读取数据时更复杂。

如果您需要同时管理数千个打开的连接,每个只发送一些数据,例如聊天服务器,在 NIO 中实现服务器可能是一个优势。同样,如果您需要与其他计算机保持大量开放连接,例如在 P2P 网络中,使用单个线程来管理所有出站连接可能是一个优势。

此图中说明了这一个线程,多个连接设计:

▲ Java NIO:管理多个连接的单个线程

如果您拥有较少带宽的连接,一次发送大量数据,那么可能最经典的 IO 服务器实现可能是最合适的。

此图说明了经典的 IO 服务器设计:

▲ Java IO:经典的 IO 服务器设计 – 由一个线程处理的一个连接

7、更简化的理解

以众所周之的数据读取过程为例,我们来一个更简化的理解。

对于数据读取,就读取速度来说:CPU > 内存 > 硬盘。

I- 就是从硬盘到内存

O- 就是从内存到硬盘

第一种方式:从硬盘读取数据,然后程序一直等,数据读完后,继续你的操作。这种方式是最简单的,叫阻塞 IO(也就是经典 IO)。

第二种方式:从硬盘读取数据,然后程序继续向下执行,等数据读取完后,通知当前程序读取完成(对硬件来说叫中断,对程序来说叫回调),然后此程序可以立即处理读取的数据,也可以执行完当前操作后再对读取完的数据进行操作。

8、总而言之

还是以数据读取为例,操作系统是按块 Block(块)从硬盘拿数据,就如同一个大脸盆,一下子就放入了一盆水。但是,当 Java 使用的时候,旧的 IO(经典 IO)确实基于 流 Stream 的,也就是虽然操作系统给我了一脸盆水,但是我得用吸管慢慢喝。

由于经典 IO 的重重落后理念,于是,NIO 横空出世。。。

附录:更多 NIO 异步网络编程资料

《Java 新一代网络编程模型 AIO 原理及 Linux 系统 AIO 介绍》

《有关“为何选择 Netty”的 11 个疑问及解答》

《开源 NIO 框架八卦——到底是先有 MINA 还是先有 Netty?》

《选 Netty 还是 Mina:深入研究与对比(一)》

《选 Netty 还是 Mina:深入研究与对比(二)》

《NIO 框架入门(一):服务端基于 Netty4 的 UDP 双向通信 Demo 演示》

《NIO 框架入门(二):服务端基于 MINA2 的 UDP 双向通信 Demo 演示》

《NIO 框架入门(三):iOS 与 MINA2、Netty4 的跨平台 UDP 双向通信实战》

《NIO 框架入门(四):Android 与 MINA2、Netty4 的跨平台 UDP 双向通信实战》

《Netty 4.x 学习(一):ByteBuf 详解》

《Netty 4.x 学习(二):Channel 和 Pipeline 详解》

《Netty 4.x 学习(三):线程模型详解》

《Apache Mina 框架高级篇(一):IoFilter 详解》

《Apache Mina 框架高级篇(二):IoHandler 详解》

《MINA2 线程原理总结(含简单测试实例)》

《Apache MINA2.0 开发指南(中文版)[附件下载]》

《MINA、Netty 的源代码(在线阅读版)已整理发布》

《解决 MINA 数据传输中 TCP 的粘包、缺包问题(有源码)》

《解决 Mina 中多个同类型 Filter 实例共存的问题》

《实践总结:Netty3.x 升级 Netty4.x 遇到的那些坑(线程篇)》

《实践总结:Netty3.x VS Netty4.x 的线程模型》

《详解 Netty 的安全性:原理介绍、代码演示(上篇)》

《详解 Netty 的安全性:原理介绍、代码演示(下篇)》

《详解 Netty 的优雅退出机制和原理》

《NIO 框架详解:Netty 的高性能之道》

《Twitter:如何使用 Netty 4 来减少 JVM 的 GC 开销(译文)》

《绝对干货:基于 Netty 实现海量接入的推送服务技术要点》

《Netty 干货分享:京东京麦的生产级 TCP 网关技术实践总结》

《新手入门:目前为止最透彻的的 Netty 高性能原理和框架架构解析》

《写给初学者:Java 高性能 NIO 框架 Netty 的学习方法和进阶策略》

《少啰嗦!一分钟带你读懂 Java 的 NIO 和经典 IO 的区别》

更多同类文章 ……

(本文同步发布于:http://www.52im.net/thread-26…)

退出移动版