目录:
Java NIO 学习笔记(一)----概述,Channel/Buffer
Java NIO 学习笔记(二)----汇集和分散,通道到通道
Java NIO 学习笔记(三)----Selector
Java NIO 学习笔记(四)----文件通道和网络通道
Java NIO 学习笔记(五)----路径、文件和管道 Path/Files/Pipe
Java NIO 学习笔记(六)----异步文件通道 AsynchronousFileChannel
Java NIO 学习笔记(七)----NIO/IO 的对比和总结html
Java NIO (来自 Java 1.4)能够替代标准 IO 和 Java Networking API ,NIO 提供了与标准 IO 不一样的使用方式。学习 NIO 以前建议先掌握标准 IO 和 Java 网络编程,推荐教程:java
本文目的: 掌握了标准 IO 以后继续学习 NIO 知识。主要参考 JavaDoc 和 Jakob Jenkov 的英文教程 Java NIO Tutorial编程
NIO 由如下核心组件组成:数组
通道和缓冲区
在标准 IO API 中,使用字节流和字符流。 在 NIO 中使用通道和缓冲区。 数据老是从通道读入缓冲区,或从缓冲区写入通道。服务器
非阻塞IO
NIO 能够执行非阻塞 IO 。 例如,当通道将数据读入缓冲区时,线程能够执行其余操做。 而且一旦数据被读入缓冲区,线程就能够继续处理它。 将数据写入通道也是如此。网络
选择器
NIO 包含“选择器”的概念。 选择器是一个能够监视多个事件通道的对象(例如:链接打开,数据到达等)。 所以,单个线程能够监视多个通道的数据。app
NIO 有比这些更多的类和组件,但在我看来,Channel,Buffer 和 Selector 构成了 API 的核心。 其他的组件,如 Pipe 和 FileLock ,只是与三个核心组件一块儿使用的实用程序类。dom
一般,NIO 中的全部 IO 都以 Channel 开头,频道有点像流。 数据能够从 Channel 读入 Buffer,也能够从 Buffer 写入 Channel :
异步
有几种 Channel 和 Buffer ,如下是 NIO 中主要 Channel 实现类的列表,这些通道包括 UDP + TCP 网络 IO 和文件 IO:学习
这些类也有一些有趣的接口,但为了简单起见,这里暂时不提,后续会进行学习的。
如下是 NIO 中的核心 Buffer 实现,其实就是 7 种基本类型:
NIO 还有一个 MappedByteBuffer,它与内存映射文件一块儿使用,一样这个后续再讲。
选择器容许单个线程处理多个通道。 若是程序打开了许多链接(通道),但每一个链接只有较低的流量,使用选择器就很方便。 例如,在聊天服务器中, 如下是使用 Selector 处理 3 个 Channel 的线程图示:
要使用选择器,须要使用它注册通道。 而后你调用它的 select() 方法。 此方法将阻塞,直到有一个已注册通道的事件准备就绪。 一旦该方法返回,该线程就能够处理这些事件。 事件能够是传入链接,接收数据等。
NIO 通道相似于流,但有一些区别:
如上所述,NIO 中老是将数据从通道读取到缓冲区,或将数据从缓冲区写入通道。 这是一个例子:
// 文件内容是 123456789 RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw"); FileChannel fileChannel = accessFile.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(48); int data = fileChannel.read(buffer); // 将 Channel 的数据读入缓冲区,返回读入到缓冲区的字节数
使用 Buffer 与 Channel 交互,数据从通道读入缓冲区,或从缓冲区写入通道。
缓冲区本质上是一个能够写入数据的内存块,以后能够读取数据。 Buffer 对象包装了此内存块,提供了一组方法,能够更轻松地使用内存块。
使用 Buffer 读取和写入数据一般遵循如下四个步骤:
将数据写入Buffer 时,Buffer 会跟踪写入的数据量。 当须要读取数据时,就使用 flip() 方法将缓冲区从写入模式切换到读取模式。 在读取模式下,缓冲区容许读取写入缓冲区的全部数据。
读完全部数据以后,就须要清除缓冲区,以便再次写入。 能够经过两种方式执行此操做:经过调用 clear() 或调用 compact() 。区别在于 clear() 是方法清除整个缓冲区,而 compact() 方法仅清除已读取的数据,未读数据都会移动到缓冲区的开头,新数据将在未读数据以后写入缓冲区。
这是一个简单的缓冲区用法示例:
public class ChannelExample { public static void main(String[] args) throws IOException { // 文件内容是 123456789 RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw"); FileChannel fileChannel = accessFile.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(48); //建立容量为48字节的缓冲区 int data = fileChannel.read(buffer); // 将 Channel 的数据读入缓冲区,返回读入到缓冲区的字节数 while (data != -1) { System.out.println("Read " + data); // Read 9 buffer.flip(); // 将 buffer 从写入模式切换为读取模式 while (buffer.hasRemaining()) { System.out.print((char) buffer.get()); // 每次读取1byte,循环输出 123456789 } buffer.clear(); // 清除当前缓冲区 data = fileChannel.read(buffer); // 将 Channel 的数据读入缓冲区 } accessFile.close(); } }
缓冲区有 3 个须要熟悉的属性,以便了解缓冲区的工做原理。 这些是:
另外还有标记 mark ,
标记、位置、限制和容量值遵照如下不变式:
0 <= mark<= position <= limit<= capacity
position 和 limit 的含义取决于 Buffer 是处于读取仍是写入模式。 不管缓冲模式如何,capacity 老是同样的表示容量。
如下是写入和读取模式下的容量,位置和限制的说明:
做为存储器块,缓冲区具备必定的固定大小,也称为“容量”。 只能将 capacity 多的 byte,long,char 等写入缓冲区。 缓冲区已满后,须要清空它(读取数据或清除它),而后才能将更多数据写入。
将数据写入缓冲区时,能够在某个位置执行操做。 position 初始值为 0 ,当一个 byte,long,char 等已写入缓冲区时,position 被移动,指向缓冲区中的下一个单元以插入数据。 position 最大值为 capacity -1
从缓冲区读取数据时,也能够从给定位置开始读取数据。 当缓冲区从写入模式切换到读取模式时,position 将重置为 0 。当从缓冲区读取数据时,将从 position 位置开始读取数据,读取后会将 position 移动到下一个要读取的位置。
在写入模式下,Buffer 的 limit 是能够写入缓冲区的数据量的限制,此时 limit=capacity。
将缓冲区切换为读取模式时,limit 表示最多能读到多少数据。 所以,当将 Buffer 切换到读取模式时,limit被设置为以前写入模式的写入位置(position ),换句话说,你能读到以前写入的全部数据(例如以前写写入了 6 个字节,此时 position=6 ,而后切换到读取模式,limit 表明最多能读取的字节数,所以 limit 也等于 6)。
要获取 Buffer 对象,必须先分配它。 每一个 Buffer 类都有一个 allocate() 方法来执行此操做。 下面是一个显示ByteBuffer分配的示例,容量为48字节:
ByteBuffer buffer = ByteBuffer.allocate(48); //建立容量为48字节的缓冲区
能够经过两种方式将数据写入 Buffer:
这是一个示例,显示了 Channel 如何将数据写入 Buffer:
int data = fileChannel.read(buffer); // 将 Channel 的数据读入缓冲区,返回读入到缓冲区的字节数 buffer.put(127); // 此处的 127 是 byte 类型
put() 方法有许多其余版本,容许以多种不一样方式将数据写入 Buffer 。 例如,在特定位置写入,或将一个字节数组写入缓冲区。
flip() 方法将 Buffer 从写入模式切换到读取模式。 调用 flip() 会将 position 设置回 0,并将 limit 的值设置为切换以前的 position 值。换句话说,limit 表示以前写进了多少个 byte、char 等 —— 如今能读取多少个 byte、char 等。
有两种方法能够从 Buffer 中读取数据:
如下是将缓冲区中的数据读入通道的示例:
int bytesWritten = fileChannel.write(buffer); byte aByte = buffer.get();
和 put() 方法同样,get() 方法也有许多其余版本,容许以多种不一样方式从 Buffer 中读取数据。有关更多详细信息,请参阅JavaDoc以获取具体的缓冲区实现。
如下列出 ByteBuffer 类的部分方法:
方法 | 描述 |
---|---|
byte[] array() | 返回实现此缓冲区的 byte 数组,此缓冲区的内容修改将致使返回的数组内容修改,反之亦然。 |
CharBuffer asCharBuffer() | 建立此字节缓冲区做为新的独立的char 缓冲区。新缓冲区的内容将今后缓冲区的当前位置开始 |
XxxBuffer asXxxBuffer() | 同上,建立对应的 Xxx 缓冲区,Xxx 可为 Short/Int/Long/Float/Double |
byte get() | 相对 get 方法。读取此缓冲区当前位置的字节,而后该 position 递增。 |
ByteBuffer get(byte[] dst, int offset, int length) | 相对批量 get 方法,后2个参数可省略 |
byte get(int index) | 绝对 get 方法。读取指定索引处的字节。 |
char getChar() | 用于读取 char 值的相对 get 方法。 |
char getChar(int index) | 用于读取 char 值的绝对 get 方法。 |
xxx getXxx(int index) | 用于读取 xxx 值的绝对 get 方法。index 能够选,指定位置。 |
众多 put() 方法 | 参考以上 get() 方法 |
static ByteBuffer wrap(byte[] array) | 将 byte 数组包装到缓冲区中。 |
Buffer对象的 rewind() 方法将 position 设置回 0,所以能够重读缓冲区中的全部数据, limit 则保持不变。
若是调用 clear() ,则将 position 设置回 0 ,并将 limit 被设置成 capacity 的值。换句话说,Buffer 被清空了。 可是 Buffer 中的实际存放的数据并未清除。
若是在调用 clear() 时缓冲区中有任何未读数据,数据将被“遗忘”,这意味着再也不有任何标记告诉读取了哪些数据,尚未读取哪些数据。
若是缓冲区中仍有未读数据,而且想稍后读取它,但须要先写入一些数据,这时候应该调用 compact() ,它会将全部未读数据复制到 Buffer 的开头,而后它将 position 设置在最后一个未读元素以后。 limit 属性仍设置为 capacity ,就像 clear() 同样。 如今缓冲区已准备好写入,而且不会覆盖未读数据。
以经过调用 Buffer 对象的 mark() 方法在 Buffer 中标记给定位置。 而后,能够经过调用 Buffer.reset() 方法将位置重置回标记位置,就像在标准 IO 中同样。
buffer.mark(); // 调用 buffer.get() 等方法读取数据... buffer.reset(); // 设置 position 回到 mark 位置。
可使用 equals() 和 compareTo() 比较两个缓冲区。
equals() 成立的条件:
如上,equals 仅比较缓冲区的一部分,而不是它内部的每一个元素。 实际上,它只是比较缓冲区中的其他元素。
compareTo() 方法比较两个缓冲区的剩余元素(字节,字符等), 在下列状况下,一个 Buffer 被视为“小于”另外一个 Buffer: