在前面三篇关于NIO系列的学习文章:核心概念及基本读写 、缓冲区内部实现机制 、连网和异步IO 中,我们已经介绍了NIO的核心知识,本文继续探讨和学习缓冲区更多特性及分散/聚集IO等相关内容。
7. 缓冲区更多内容
到目前为止,我们已经学习了使用缓冲区进行日常工作所需要掌握的大部分内容。我们所举的例子也没怎么超出标准的读/写过程种类,在原来的I/O中可以像在NIO中一样容易地实现这样的标准读写过程。
在本节将讨论使用缓冲区的一些更复杂的方面,比如缓冲区分配、包装和分片。我们还会讨论NIO带给Java平台的一些新功能。我们将学如何创建不同类型的缓冲区以达到不同的目的,如可保护数据不被修改的“只读缓冲区”,和直接映射到底层操作系统缓冲区的“直接缓冲区”,以及如何在 NIO 中创建内存映射文件。
1) 缓冲区分配和包装
在能够读和写之前,必须有一个缓冲区。要创建缓冲区,您必须“分配”它。我们使用静态方法allocate()来分配缓冲区:
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
allocate()方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中,在本例中是一个ByteBuffer。
您还可以将一个现有的数组转换为缓冲区,如下所示:
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );
本例使用了wrap()方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装,底层数据就可以通过缓冲区或者直接访问。
2) 缓冲区分片
slice()方法根据现有的缓冲区创建一个子缓冲区。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。
使用例子可以最好地说明这点。让我们首先创建一个长度为10的ByteBuffer:
ByteBuffer buffer = ByteBuffer.allocate( 10 );
然后使用数据来填充这个缓冲区,在第n个槽中放入数字n:
for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}
现在我们对这个缓冲区“分片”,以创建一个包含槽3到槽6的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个窗口 。
窗口的起始和结束位置通过设置position和limit值来指定,然后调用Buffer的slice()方法进行分片:
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
该“片段”是缓冲区的子缓冲区。不过,“片段”和“缓冲区”共享同一个底层数据数组,我们在下一节将会看到这一点。
3) 缓冲区片份和数据共享
我们已经创建了原缓冲区的子缓冲区,并且已经知道缓冲区和子缓冲区共享同一个底层数据数组。让我们看看这意味着什么。
我们遍历子缓冲区,将每一个元素乘以11来改变它。例如,5会变成55。
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 11;
slice.put( i, b );
}
最后,再看一下原缓冲区中的内容:
buffer.position( 0 );
buffer.limit( buffer.capacity() );
while (buffer.remaining()>0) {
System.out.println( buffer.get() );
}
结果表明只有在子缓冲区窗口中的元素被改变了:
0
1
2
33
44
55
66
7
8
9
缓冲区片对于促进抽象非常有帮助。可以编写自己的函数处理整个缓冲区,而且如果想要将这个过程应用于子缓冲区上,您只需取主缓冲区的一个片,并将它传递给您的函数。这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易。
4) 只读缓冲区
只读缓冲区的含义已经很直白了:您可以读取它们,但是不能向它们写入。可以通过调用缓冲区的asReadOnlyBuffer()方法,来将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。
只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。不能将只读的缓冲区转换为可写的缓冲区。
5) 直接和间接缓冲区
另一种有用的ByteBuffer是直接缓冲区。“直接缓冲区”是为加快I/O速度,而以一种特殊的方式分配其内存的缓冲区。实际上,直接缓冲区的准确定义是与实现相关的。
Sun的文档是这样描述直接缓冲区的: 给定一个直接字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作。也就是说,它会在每一次调用底层操作系统的本机I/O操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。
附件中,您可以在例子程序FastCopyFile.java中看到直接缓冲区的实际应用,这个程序是CopyFile.java的另一个版本,它使用了直接缓冲区以提高速度。还可以用内存映射文件创建直接缓冲区。
6) 内存映射文件I/O
内存映射文件I/O是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的I/O快得多。
内存映射文件I/O是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会送入(或者映射)到内存中。
内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
尽管创建内存映射文件相当简单,但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将 数据保存到磁盘是没有分开的。
7) 将文件映射到内存
了解内存映射的最好方法是使用例子。在下面的例子中,我们要将一个FileChannel (它的全部或者部分)映射到内存中。为此我们将使用FileChannel.map()方法。下面代码行将文件的前1024个字节映射到内存中:
MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );
map()方法返回一个MappedByteBuffer,它是ByteBuffer的子类。因此,您可以像使用其他任何ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。
8. 分散和聚集
1) 概述:
分散/聚集I/O是使用多个而不是单个缓冲区来保存数据的读写方法。
一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区数组而不是向 单个缓冲区写入数据。
分散/聚集I/O对于将数据流划分为单独的部分很有用,这有助于实现复杂的数据格式。
2) 分散/聚集 I/O:
通道可以有选择地实现两个新的接口:ScatteringByteChannel和GatheringByteChannel。一个 ScatteringByteChannel是一个具有两个附加读方法的通道:
long read( ByteBuffer[] dsts );
long read( ByteBuffer[] dsts, int offset, int length );
这些long read()方法很像标准的read方法,只不过它们不是取单个缓冲区而是取一个缓冲区数组。
在“分散读取”中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。
3) 分散/聚集的应用:
分散/聚集I/O对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这 两个缓冲区中。
我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据,所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。
4) 聚集写入:
聚集写入类似于分散读取,只不过是用来写入。它也有接受缓冲区数组的方法:
long write( ByteBuffer[] srcs );
long write( ByteBuffer[] srcs, int offset, int length );
聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的消息例子保持一致,您可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流,以便跨越网络传输消息。
从附件的例子程序 UseScatterGather.java 中可以看到分散读取和聚集写入的实际应用。
后续: 在下一篇文章中,会介绍NIO相关的文件锁定、字符集等知识。有兴趣的可以共同学习、讨论。
分享到:
相关推荐
#### 缓冲区更多特性及分散/聚集IO NIO中的缓冲区不仅支持基本的读写操作,还提供了额外的功能,如直接缓冲区、清理缓冲区等,这些功能可以帮助优化内存管理和提高性能。分散/聚集IO允许将多个缓冲区中的数据一次性...
##### 1.4 缓冲区更多特性及分散/聚集IO 缓冲区除了基本的读写操作外,还支持许多高级特性,例如直接缓冲区(Direct Buffer),这种缓冲区直接在本地内存中分配,避免了 JVM 的堆内存和本地内存之间的复制,提高了...
分散/聚集是指在一次读写操作中,数据可以从多个缓冲区分散读取,或者向多个缓冲区聚集写入,提高了数据处理的灵活性。 6. **通道间的数据传输** NIO允许直接在通道之间进行数据传输,无需经过缓冲区,减少了数据...
而 Java NIO 面向缓冲区,数据读取到缓冲区之后可以在缓冲区内前后移动,提供了更大的灵活性。 - **阻塞与非阻塞**:Java IO 默认是阻塞式的,这意味着当读取或写入操作正在进行时,线程会被挂起等待操作完成;而 ...
6. **scatter/gather(分散/聚集)**:这一特性允许数据从多个缓冲区写入通道,或者从通道读取到多个缓冲区,提高了数据处理的灵活性。 通过阅读这些网文,你将能够深入理解Java NIO的原理、用法和优势,并能够将其...
这是一种分散/聚集读写技术,可以一次操作多个缓冲区,提高数据处理效率。scatter操作是从通道读取数据到多个缓冲区,gather则是从多个缓冲区写入通道。 8. **内存映射文件(Memory-Mapped File)** 这是一种将...
7. **scatter/gather(分散/聚集)**:NIO允许数据从多个缓冲区分散写入一个通道,或者从一个通道聚集读入多个缓冲区,这对于处理多个小数据块的传输非常有效。 8. **异步文件操作**:Java NIO2(Java 7引入)引入...
8. **Scattering and Gathering(分散与聚集)**:NIO支持将数据分散写入多个缓冲区,或者从多个缓冲区聚集读取数据,这对于处理网络传输和文件读写非常有用。 总的来说,Java NIO提供了一套高效、灵活的I/O机制,...
分散读取和聚集写入是NIO的特性之一,允许将数据从多个缓冲区分散读取到通道,或者从通道聚集写入到多个缓冲区,这对于处理网络数据包或文件分块等场景非常有用。 8. **NIO与网络编程** 在网络编程中,NIO主要...
- 散集(Scatter)是从一个通道分散地读取数据到多个缓冲区,而聚集(Gather)是从多个缓冲区聚合数据到一个通道。这种操作在处理多个小块数据时非常有用。 9. **文件锁定** - Java NIO提供了文件锁定功能,可以...
散集(Scatter)是指从一个通道读取的数据分散到多个缓冲区,而聚集(Gather)则是将多个缓冲区的数据写入一个通道。这种方式允许数据的灵活组合和传输,减少了数据复制。 7. **字符集转换** NIO提供了一种统一的...
14. **Scattering and Gathering**:NIO支持Scattering(分散)和Gathering(聚集)读写,可以从多个缓冲区写入通道,也可以从通道读入到多个缓冲区。 以上是根据标题和描述推测的IO流面试题可能涉及的内容。面试时...