在前面三篇关于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相关的文件锁定、字符集等知识。有兴趣的可以共同学习、讨论。
分享到:
相关推荐
NULL 博文链接:https://zhangshixi.iteye.com/blog/683767
NULL 博文链接:https://zhangshixi.iteye.com/blog/679959
NULL 博文链接:https://zhangshixi.iteye.com/blog/685022
nio 学习 demo 解决沾包问题 处理因缓冲区满导致写入失败问题 处理因缓冲区满导致写入失败问题 ,代码里面应该在写文件结束后取消掉注册的事件
代码经过压力测试,采用反应器模式,没有第三方框架,实现功能:可以一个线程处理多个请求,也可以加上多线程。处理数据采用的多线程。实现功能:基于HTTP协议,解析请求和拼接响应,基于NIO的非阻塞,线程池,文件...
Netty (netty-netty-5.0.0.Alpha2.tar.gz)是一个 NIO 客户端服务器框架,可以快速轻松地开发协议服务器和客户端等网络应用程序。它极大地简化和流线了网络编程,例如 TCP 和 UDP 套接字服务器。 “快速和简单”并...
Java NIO通道:通道基础、文件通道、Socket通道、工具类 Java NIO缓冲区:基础、缓冲区(Buffer)、创建缓冲区、直接缓冲区(DirectByteBuffer) Java NIO选择器:核心概念、选择器使用、Demo、选择器深入、
java学习笔记1(java io/nio)设计模式
NULL 博文链接:https://aga.iteye.com/blog/206691
Contents: 1 核心概念以及基本读写 2 缓冲区的实现机制 3 连网与异步IO 4 分散和聚集IO 5 文件锁定
NULL 博文链接:https://utopialxw.iteye.com/blog/1138133
IO和NIO区别
本项目示例基于spring boot 最新版本(2.1.9)实现,Spring Boot、Spring Cloud 学习示例,将持续更新…… 在基于Spring Boot、Spring Cloud 分布微服务开发过程中,根据实际项目环境,需要选择、集成符合项目...
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。 Java NIO: Non-blocking IO(非阻塞IO) Java NIO...
NULL 博文链接:https://ilrxx.iteye.com/blog/1051402
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。 Java NIO: Asynchronous IO(异步IO) Java NIO可以...
It's nio based (using my opensource nio framework :yanf4j), and was carefully tuned to get top performance. homepage: http://code.google.com/p/xmemcached/ downloads; ...
1、Java IO_NIO 2、Java+IO.pdf