前面的一些文章中我总结了一些 Java IO 和 NIO 相关的主要知识点, 也是管中窥豹, IO 类库已经功能很强大了, 但是 Java 为什么又要引入 NIO, 这是我一直不是很清楚的? 前面也只是简单提及了一下: 因为性能, 但是仅仅是因为性能吗, 除此之外是否还有别的原因, 或者说既然 NIO 性能好, 那为什么现在我们还在使用 IO. 本节我们就来详细对比一下两者的特性以及两者之间的不一致对我们编码所带来的影响.
同样, 本文会主要围绕下面几个方面来总结:
Java NIO 和 IO 的主要区别
NIO 和 IO 的不同对代码设计带来的变化
两种 IO 的各自适用场景
总结
1. Java NIO 和 IO 的主要区别
两者之间的不同主要体现在如下三个方面:
Java IO 是面向流 (Stream) 的, 而 Java NIO 是面向缓冲区 (Buffer) 的;
IO 模型的不同, Java IO 是属于阻塞式 IO(Blocking IO), 而 Java NIO 是属于非阻塞式 IO(Non Blocking IO);
Java NIO 中还引入了 Selector 的概念, 可以实现多路复用;
在接下来的部分, 我们逐个讨论这三个不同.
1.1 面向流与面向缓冲区
Java NIO 和 IO 之间第一个不同点是 IO 是面向流 (Stream) 的而 NIO 是面向缓冲区 (Buffer) 的.
Java IO 是面向流的, 这意味着是一次性从流中读取一批数据, 这些数据并不会缓存在任何地方, 并且对于在流中的数据是不支持在数据中前后移动. 如果需要在这些数据中移动(为什么要移动, 可以多次读取), 则还是需要将这部分数据先缓存在缓冲区中.
而 Java NIO 采用的是面向缓冲区的方式, 有些不同, 数据会先读取到缓冲区中以供稍后处理. 在 buffer 中是可以方便地前移和后移, 这使得在处理数据时可以有更大的灵活性. 但是呢需要检查 buffer 是否包含需要的所有数据以便能够将其完整地处理, 并且需要确保在通过 channel 往 buffer 读数据的时候不能够覆盖还未处理的数据.
1.2 IO 模型的区别
Java IO 中使用的流是属于阻塞式的, 意味着当线程调用其 read()或 write()方法时线程会阻塞, 直到完成了数据的读写, 在读写的过程中线程是什么都做不了的.
Java NIO 提供了一种非阻塞模式, 使得线程向 channel 请求读数据时, 只会获取已经就绪的数据, 并不会阻塞以等待所有数据都准备好(IO 就是这样做), 这样在数据准备的阶段线程就能够去处理别的事情. 对于非阻塞式写数据是一样的. 线程往 channel 中写数据时, 并不会阻塞以等待数据写完, 而是可以处理别的事情, 等到数据已经写好了, 线程再处理这部分事情.
当线程在进行 IO 调用并且不会进入阻塞的情况下, 这部分的空余时间就可以花在和其他 channel 进行 IO 交互上. 也就是说, 这样单个线程就能够管理多个 channel 的输入和输出了.
1.3 Selector
Java NIO 中的 Selector 允许单个线程监控多个 channel, 可以将多个 channel 注册到一个 Selector 中, 然后可以 "select" 出已经准备好数据的 channel, 或者准备好写入的 channel. 这个 selector 机制使得单个线程同时管理多个 channel 变得更容易.
2. NIO 和 IO 的不同对代码设计带来的变化
选择使用 NIO 还是 IO 作为开发工具包会在如下几个方面影响应用设计:
API 是调用 NIO 类库还是 IO 类库;
数据的处理方式;
用来处理数据的线程的数量;
2.1 API 的调用
采用 NIO 的 API 调用方式和 IO 是不一样的, 与直接从 InputStream 中读取字节数据不同, 在 NIO 中, 数据必须要先被读到 buffer 中, 然后再从那里进行后续的处理.
2.2 数据的处理方式
采用 NIO 的设计还是 IO 的设计, 数据的处理方式也是不一样的.
在 IO 设计中是从 InputStream 或 Reader 中逐字节读取数据. 在下面例子中, 我们通过一个处理基于文本的简单例子来说明两种设计的区别:
- Name: Anna
- Age: 25
- Email: anna@mailserver.com
- Phone: 1234567890
采用 IO 的方式, 这些数据流会像下面这样处理:
- InputStream input = ... ; // get the InputStream from the client socket
- BufferedReader reader = new BufferedReader(new InputStreamReader(input));
- String nameLine = reader.readLine();
- String ageLine = reader.readLine();
- String emailLine = reader.readLine();
- String phoneLine = reader.readLine();
注意在这里处理状态是通过程序执行了多少就能够确定的. 换句话说, 当第一行 reader.readLine()返回之后, 可以确定已经读了一整行. 因为 readLine()会阻塞直到整行数据读完. 而且我们能够确切地知道所读取的这第一行是包含名字的. 类似, 第二次调用 readLine()返回之后我们确切地知道所读取的内容包含年龄.
可以知道, 上面的程序只有当有新的数据是可读时才会进行处理, 在每一步都知道数据是什么. 一旦执行读写的线程已经读取了一些数据之后, 是不能够再返回到前面的数据(因为流的方式只能读取一次, 很好理解, 像水一样, 流完了就流完了, 除非你把它装到容器里面). 上面程序中所遵循的原则如下图所示:
而 NIO 的实现则看起来有些不同, 如下:
- ByteBuffer buffer = ByteBuffer.allocate(48);
- int bytesRead = inChannel.read(buffer);
注意第二行是从 channel 读取数据到 buffer 中, 当 read()方法返回时我们是不知道是否所有需要的数据有没有全部读到 buffer 中, 我们知道的只是 buffer 中可能包含一部分数据, 这会使得整个过程的处理有点麻烦.
假设, 在第一次调用 read()之后, 所有读到 buffer 中的数据只有半行, 比如,"Name:An". 这时可以处理数据吗, 显然是不可以的(因为还没有读完), 需要等到至少一行数据被读到 buffer 中.
那么我们又如何来知道 buffer 中包含足够可以处理的数据呢? 唯一的办法只有检查 buffer 中的数据了. 所以结果就是我们需要通过多次检查 buffer 中的数据来判断数据是否已经全部读进 buffer 了. 这样就很低效, 而且容易导致程序设计混乱. 比如:
- ByteBuffer buffer = ByteBuffer.allocate(48);
- int bytesRead = inChannel.read(buffer);
- while(! bufferFull(bytesRead) ) {
- bytesRead = inChannel.read(buffer);
- }
bufferFull()方法会跟踪有多少数据被读到 buffer 中了, 并且返回 true 或者 false, 取决于 buffer 是否已满. 换言之, 如果 buffer 中的数据已经可供处理, 那就代表它已经满了.
bufferFull()方法会扫描整个 buffer, 要保证扫描并不会影响整个 buffer 的状态, 不然可能导致后面要读入 buffer 中的数据不能读到正确地位置. 这并非不可能, 所以对于设计者来说这是一个需要关注的地方.
如果 buffer 已满, 那其中的数据就可供处理. 如果没满, 那可能需要部分地处理那些数据(如果需要的话), 只是在大部分场景下是不需要的.
下图描述了这种 is-data-in-buffer-ready 的循环:
3. 两种 IO 的各自适用场景
NIO 使得通过单个或少量线程来管理多个 channel(网络连接或者文件)成为可能, 但是代价是传递数据会比从阻塞的流中读数据更复杂. 我们学习一项新的技术时, 既要看到其优点也要看到其缺点.
如果需要同时管理数以千计的连接, 而且每个连接只会发送少量的数据, 比如聊天服务器, 用 NIO 的方式来实现这个服务器则比较合适. 类似的, 如果需要长时间保持一些和别的电脑的连接, 比如在一个 P2P 网络中, 用单个线程来管理所有的对外连接也有优势. 如下图描述了这种单个线程, 多个连接的设计模型:
如果只有少量的连接, 但是每个连接又都占用大量的带宽, 短时间之内发送大量数据, 这时后也许传统的 IO 模型会更适用, 因为专一, 所以在特定场景下可以更高效. 如下图描述了一个基于传统 IO 模型设计的服务器模型:
4. 总结
在前面总结了很多 IO 和 NIO 的相关知识之后, 本文总结了 Java 中两种 IO 类库的区别即各自的优缺点:
传统 Java IO 是面向流, 从流中读取数据或者写入到流中, 而 Java NIO 是面向缓冲区的, 通过 channel 和 buffer 的搭配使用来读取或者写入数据;
面向流只能一次读取数据; 面向缓冲区可以多次读取数据;
面向流的方式处理数据过程相对简单, 易于实现; 而 Java NIO 中面向 buffer 的方式一般是非阻塞的方式, 所以在数据的操作上会更复杂, 从而会增加代码的复杂程度;
Java NIO 提供了 Selector 的概念, 可以通过少量线程处理多个连接, 可以有效处理并发; 而 Java IO 则专注于单个线程阻塞式读写, 对于少量连接但是每个连接都占用大量宽带的场景更适用;
技术没有好坏, 只有合适与否!
来源: https://www.cnblogs.com/volcano-liu/p/11143543.html