2.6.1 内存映射文件的性能
在本节的末尾,你可以发现一个计算传统的文件输入和内存映射文件的CRC32校验和的程序。在同一台机器上,我们对JDK的jre/lib目录中的37MB的rt.jar文件用不同的方式来计算校验和,记录下来的时间数据如表2-5所示。
表2-5 文件操作的处理时间数据
正如你所见,在这台特定的机器上,内存映射比使用带缓冲的顺序输入要稍微快一点,但是比使用RandomAccessFile快很多。
当然,精确的值因机器不同会产生很大的差异,但是很明显,与随机访问相比,性能提高总是很显著的。另一方面,对于中等尺寸文件的顺序读入则没有必要使用内存映射。
java.nio包使内存映射变得十分简单,下面就是我们需要做的。
首先,从文件中获得一个通道(channel),通道是用于磁盘文件的一种抽象,它使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。
然后,通过调用FileChannel类的map方法从这个通道中获得一个ByteBuffer。你可以指定想要映射的文件区域与映射模式,支持的模式有三种:
·FileChannel.MapMode.READ_ONLY:所产生的缓冲区是只读的,任何对该缓冲区写入的尝试都会导致ReadOnlyBufferException异常。
·FileChannel.MapMode.READ_WRITE:所产生的缓冲区是可写的,任何修改都会在某个时刻写回到文件中。注意,其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于操作系统的。
·FileChannel.MapMode.PRIVATE:所产生的缓冲区是可写的,但是任何修改对这个缓冲区来说都是私有的,不会传播到文件中。
一旦有了缓冲区,就可以使用ByteBuffer类和Buffer超类的方法读写数据了。
缓冲区支持顺序和随机数据访问,它有一个可以通过get和put操作来移动的位置。例如,可以像下面这样顺序遍历缓冲区中的所有字节:
或者,像下面这样进行随机访问:
你可以用下面的方法来读写字节数组:
最后,还有下面的方法:
用来读入在文件中存储为二进制值的基本类型值。正如我们提到的,Java对二进制数据使用高位在前的排序机制,但是,如果需要以低位在前的排序方式处理包含二进制数字的文件,那么只需调用
要查询缓冲区内当前的字节顺序,可以调用:
警告:这一对方法没有使用set/get命名惯例。
要向缓冲区写数字,可以使用下列的方法:
在恰当的时机,以及当通道关闭时,会将这些修改写回到文件中。
程序清单2-5用于计算文件的32位的循环冗余校验和(CRC32),这个数值就是经常用来判断一个文件是否已损坏的校验和,因为文件损坏极有可能导致校验和改变。java.util.zip包中包含一个CRC32类,可以使用下面的循环来计算一个字节序列的校验和:
注意:对CRC算法有一个很精细的解释,请查看http://www.relisoft.com/Science/CrcMath.html。
CRC计算的细节并不重要,我们只是将它作为一个有用的文件操作的实例来使用。(在实践中,每次会以更大的工夫而不是一个字节为单位来读取和更新数据,而它们的速度差异并不明显。)
应该像下面这样运行程序:
程序清单2-5 memoryMap/MemoryMapTest.java
java.io.FileInputStream 1.0
·FileChannel getChannel()1.4
返回用于访问这个输入流的通道。
java.io.FileOutputStream 1.0
·FileChannel getChannel()1.4
返回用于访问这个输出流的通道。
java.io.RandomAccessFile1.0
·FileChannel getChannel()1.4
返回用于访问这个文件的通道。
java.nio.channels.FileChannel 1.4
·static FileChannel open(Path path,OpenOption...options)7
打开指定路径的文件通道,默认情况下,通道打开时用于读入。
参数:path 打开通道的文件所在的路径
options StandardOpenOption枚举中的WRITE、APPEND、TRUNCATE_EXISTING、CREATE值
·MappedByteBuffer map(FileChannel.MapMode mode,long position,long size)
将文件的一个区域映射到内存中。
参数:mode FileChannel.MapMode类中的常量READ_ONLY、READ_WRITE、或PRIVATE之一
position 映射区域的起始位置
size 映射区域的大小
java.nio.Buffer 1.4
·boolean hasRemaining()
如果当前的缓冲区位置没有到达这个缓冲区的界限位置,则返回true。
·int limit()
返回这个缓冲区的界限位置,即没有任何值可用的第一个位置。
java.nio.ByteBuffer 1.4
·byte get()
从当前位置获得一个字节,并将当前位置移动到下一个字节。
·byte get(int index)
从指定索引处获得一个字节。
·ByteBuffer put(byte b)
向当前位置推入一个字节,并将当前位置移动到下一个字节。返回对这个缓冲区的引用。
·ByteBuffer put(int index,byte b)
向指定索引处推入一个字节。返回对这个缓冲区的引用。
·ByteBuffer get(byte[]destination)
·ByteBuffer get(byte[]destination,int offset,int length)
用缓冲区中的字节来填充字节数组,或者字节数组的某个区域,并将当前位置向前移动读入的字节数个位置。如果缓冲区不够大,那么就不会读入任何字节,并抛出BufferUnderflow Exception。返回对这个缓冲区的引用。
参数:destination 要填充的字节数组
offset 要填充区域的偏移量
length 要填充区域的长度
·ByteBuffer put(byte[]source)
·ByteBuffer put(byte[]source,int offset,int length)
将字节数组中的所有字节或者给定区域的字节都推入缓冲区中,并将当前位置向前移动写出的字节数个位置。如果缓冲区不够大,那么就不会读入任何字节,并抛出BufferUnderflow Exception。返回对这个缓冲区的引用。
参数:source 要写出的数组
offset 要写出区域的偏移量
length 要写出区域的长度
·Xxx getXxx()
·Xxx getXxx(int index)
·ByteBuffer putXxx(Xxx value)
·ByteBuffer putXxx(int index,Xxx value)
获得或放置一个二进制数。Xxx是Int、Long、Short、Char、Float或Double中的一个。
·ByteBuffer order(ByteOrder order)
·ByteOrder order()
设置或获得字节顺序,order的值是ByteOrder类的常量BIG_ENDIAN或LITTLE_ENDIAN中的一个。
·static ByteBuffer allocate(int capacity)
构建具有给定容量的缓冲区。
·static ByteBuffer wrap(byte[]values)
构建具有指定容量的缓冲区,该缓冲区是对给定数组的包装。
·CharBuffer asCharBuffer()
构建字符缓冲区,它是对这个缓冲区的包装。对该字符缓冲区的变更将在这个缓冲区中反映出来,但是该字符缓冲区有自己的位置、界限和标记。
java.nio.CharBuffer 1.4
·char get()
·CharBuffer get(char[]destination)
·CharBuffer get(char[]destination,int offset,int length)
从这个缓冲区的当前位置开始,获取一个char值,或者一个范围内的所有char值,然后将位置向前移动越过所有读入的字符。最后两个方法将返回this。
·CharBuffer put(char c)
·CharBuffer put(char[]source)
·CharBuffer put(char[]source,int offset,int length)
·CharBuffer put(String source)
·CharBuffer put(CharBuffer source)
从这个缓冲区的当前位置开始,放置一个char值,或者一个范围内的所有char值,然后将位置向前移动越过所有被写出的字符。当放置的值是从CharBuffer读入时,将读入所有剩余字符。所有方法将返回this。