`
zhangshixi
  • 浏览: 672015 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

NIO学习系列:缓冲区更多特性及分散/聚集IO

阅读更多

   在前面三篇关于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相关的文件锁定、字符集等知识。有兴趣的可以共同学习、讨论。

  • nio.zip (3.2 KB)
  • 下载次数: 340
4
2
分享到:
评论
2 楼 zhangshixi 2010-06-05  
fansfirst2008 写道
楼主很强大啊,崇拜之!
现在很多服务器,比如JETTY,TOMCAT7等等听说都用NIO写的,如果能够把它们的源代码IO部分截取过来,学习,那就太好了!

学习源码是一件很有趣的事,多钻研也会给我们带来很快的进步的。
1 楼 fansfirst2008 2010-06-05  
楼主很强大啊,崇拜之!
现在很多服务器,比如JETTY,TOMCAT7等等听说都用NIO写的,如果能够把它们的源代码IO部分截取过来,学习,那就太好了!

相关推荐

Global site tag (gtag.js) - Google Analytics