在学习了Java NIO和IO API时,很快就会想到一个问题:什么时候应该使用IO,什么时候应该使用NIO?
在本文中,我将尝试阐明Java NIO和IO之间的差异,它们的用例以及它们如何影响代码的设计。
1. Java NIO和IO的主要区别
下表总结了Java NIO和IO之间的主要区别。
IO | NIO |
---|---|
面向流 | 面向Buffer |
阻塞IO | 非阻塞IO |
Selectors |
2.面向流 vs 面向缓冲区
Java NIO和IO之间的第一个大区别是:IO是面向流的,而NIO是面向缓冲区的。
面向流的Java IO意味着一次从流中读取一个或多个字节,对读取字节的处理取决于你自己,它们不会在任何地方缓存。此外,你无法在流中的数据中来回移动,如果需要在从流中读取的数据中来回移动,则需要先将其缓存在缓冲区中。
Java NIO面向缓冲区的方法略有不同。数据被读入缓冲区,并在以后进行处理。你可以根据需要在缓冲区中来回移动,这在处理过程中更具灵活性。但是,你还需要检查缓冲区是否包含你需要的所有数据,以便对其进行完全处理。并且,还需要确保在将更多数据读入缓冲区时,不要覆盖尚未处理的缓冲区中的数据。
3.Blocking vs. Non-blocking IO
Java IO的各种流都是阻塞IO。这意味着,当线程调用read()或write()时,该线程将被阻塞,直到有一些数据要读取或数据被完全写入为止。在此期间,线程无法执行其他任何操作。
Java NIO的非阻塞模式使线程可以请求从通道读取数据,并且仅获取当前可用的数据,或者如果当前没有可用数据,则什么都不获取。线程可以继续进行其他操作,而不是在数据可供读取之前保持阻塞状态。
非阻塞写入也是如此。线程可以请求将某些数据写入通道,但不必等待将其完全写入。然后,线程可以继续运行,同时执行其他操作。
4.Selectors
Java NIO的选择器允许单个线程监视多个通道。
你可以使用选择器注册多个通道,然后使用一个线程“选择”具有可用于处理的通道,或者选择准备好进行写入的通道。这种选择器机制使单个线程可以轻松管理多个通道。
5.NIO和IO如何影响应用程序设计
选择NIO还是IO作为IO工具包可能会影响应用程序设计的以下几个方面:
API调用。
数据处理。
用于处理数据的线程数。
API调用
使用NIO时的API调用看起来与使用IO时的API调用不同,这不足为奇。IO需要从流(InputStream等)中逐字节读取,而Nio则必须首先将数据读入缓冲区,然后再从那里进行处理。
数据处理
当使用纯NIO设计而不是IO设计时,数据的处理也会受到影响。
在IO设计中,从InputStream或Reader中逐字节读取数据。假设你正在处理文本数据流:
Name: Anna Age: 25 Email: anna@mailserver.com Phone: 1234567890
文本流可以这样处理:
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()调用返回时,你知道此行包含age信息。
该程序仅在有新数据要读取时才继续运行,并且对于每个步骤,你都知道数据是什么。一旦执行线程的进度超过了读取代码中的特定数据段,该线程就不会在数据中向后移动(大多数情况下不会)。此原理如图:
NIO实现看起来会有所不同。这是一个简化的示例:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer);
请注意第二行,该行从通道读取字节到ByteBuffer。当该方法调用返回时,你不知道所需的所有数据是否都在缓冲区内,你所知道的仅仅是缓冲区包含一些字节。这使得处理有些困难。
想象一下,如果在第一次read(buffer)调用之后,读入缓冲区的所有内容都是半行。例如,“Name:An”。你可以处理这些数据吗?并不能。你需要等到至少一整行数据都进入缓冲区后,才可以处理数据。
那么如何知道缓冲区是否包含足够的数据以使其有意义呢?找出答案的唯一方法是查看缓冲区中的数据。结果是你可能必须多次检查缓冲区中的数据,然后才能知道是否有所有数据。这既效率低下,又可能使程序设计变得混乱。例如:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer); while(! bufferFull(bytesRead) ) { bytesRead = inChannel.read(buffer); }
bufferFull()方法必须跟踪读取到缓冲区中的数据量,并根据缓冲区是否已满返回true或false。换句话说,如果缓冲区已准备好进行处理,则认为缓冲区已满。
bufferFull()方法扫描缓冲区,但必须使缓冲区保持与调用bufferFull()方法之前的状态相同。如果不是,则下一个读入缓冲区的数据可能无法在正确的位置读入。这是另一个需要注意的问题。
下图说明了is-data-in-buffer-ready循环:
总结
NIO允许你仅使用一个(或几个)线程来管理多个通道(网络连接或文件),但代价是解析数据可能比从阻塞流中读取数据更为复杂。
如果你需要同时管理数千个打开的连接(每个连接仅发送少量数据),例如聊天服务器,则在NIO中实现该服务器可能是有优势的。同样,如果你需要与其他计算机保持大量开放连接,例如在P2P网络中,使用单个线程来管理所有出站连接可能具有优势。下图说明了这种单线程多连接设计:
如果只有很少的连接且具有很高的带宽,一次要发送大量数据,那么经典的IO服务器实现也许是最合适的选择。下图说明了经典的IO服务器设计:
原文链接:https://www.zhblog.net/go/java/tutorial/java-nio-vs-io?t=624