(转) Java RandomAccessFile与MappedByteBuffer

采得百花成蜜后,为谁辛苦为谁甜。这篇文章主要讲述(转) Java RandomAccessFile与MappedByteBuffer相关的知识,希望能为你提供帮助。
RandomAccessFileRandomAccessFile是用来访问那些保存数据记录的文件的,你就可以用seek( )方法来访问记录,并进行读写了。这些记录的大小不必相同;但是其大小和位置必须是可知的。但是该类仅限于操作文件。
RandomAccessFile不属于InputStream和OutputStream类系的。实际上,除了实现DataInput和DataOutput接口之外(DataInputStream和DataOutputStream也实现了这两个接口),它和这两个类系毫不相干,甚至不使用InputStream和OutputStream类中已经存在的任何功能;它是一个完全独立的类,所有方法(绝大多数都只属于它自己)都是从零开始写的。这可能是因为RandomAccessFile能在文件里面前后移动,所以它的行为与其它的I/O类有些根本性的不同。总而言之,它是一个直接继承Object的,独立的类。
基本上,RandomAccessFile的工作方式是,把DataInputStream和DataOutputStream结合起来,再加上它自己的一些方法,比如定位用的getFilePointer( ),在文件里移动用的seek( ),以及判断文件大小的length( )、skipBytes()跳过多少字节数。此外,它的构造函数还要一个表示以只读方式(“r”),还是以读写方式(“rw”)打开文件的参数 (和C的fopen( )一模一样)。它不支持只写文件。
只有RandomAccessFile才有seek搜寻方法,而这个方法也只适用于文件。BufferedInputStream有一个mark( )方法,你可以用它来设定标记(把结果保存在一个内部变量里),然后再调用reset( )返回这个位置,但是它的功能太弱了,而且也不怎么实用。
RandomAccessFile的绝大多数功能,但不是全部,已经被JDK 1.4的nio的”内存映射文件(memory-mapped files)”给取代了,你该考虑一下是不是用”内存映射文件”来代替RandomAccessFile了。

import java.io.IOException; import java.io.RandomAccessFile; public class TestRandomAccessFile { public static void main(String[] args) throws IOException { RandomAccessFile rf = new RandomAccessFile("rtest.dat", "rw"); for (int i = 0; i < 10; i++) { //写入基本类型double数据 rf.writeDouble(i * 1.414); } rf.close(); rf = new RandomAccessFile("rtest.dat", "rw"); //直接将文件指针移到第5个double数据后面 rf.seek(5 * 8); //覆盖第6个double数据 rf.writeDouble(47.0001); rf.close(); rf = new RandomAccessFile("rtest.dat", "r"); for (int i = 0; i < 10; i++) { System.out.println("Value " + i + ": " + rf.readDouble()); } rf.close(); } }

内存映射文件内存映射文件能让你创建和修改那些因为太大而无法放入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问。这种解决办法能大大简化修改文件的代码。
fileChannel.map(FileChannel.MapMode mode, long position, long size)将此通道的文件区域直接映射到内存中。注意,你必须指明,它是从文件的哪个位置开始映射的,映射的范围又有多大;也就是说,它还可以映射一个大文件的某个小片断。
MappedByteBuffer是ByteBuffer的子类,因此它具备了ByteBuffer的所有方法,但新添了force()将缓冲区的内容强制刷新到存储设备中去、load()将存储设备中的数据加载到内存中、isLoaded()位置内存中的数据是否与存储设置上同步。这里只简单地演示了一下put()和get()方法,除此之外,你还可以使用asCharBuffer( )之类的方法得到相应基本类型数据的缓冲视图后,可以方便的读写基本类型数据。
import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; public class LargeMappedFiles { static int length = 0x8000000; // 128 Mbpublic static void main(String[] args) throws Exception { // 为了以可读可写的方式打开文件,这里使用RandomAccessFile来创建文件。 FileChannel fc = new RandomAccessFile("test.dat", "rw").getChannel(); //注意,文件通道的可读可写要建立在文件流本身可读写的基础之上 MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, length); //写128M的内容 for (int i = 0; i < length; i++) { out.put((byte) ‘x‘); } System.out.println("Finished writing"); //读取文件中间6个字节内容 for (int i = length / 2; i < length / 2 + 6; i++) { System.out.print((char) out.get(i)); } fc.close(); } }

尽管映射写似乎要用到FileOutputStream,但是映射文件中的所有输出必须使用RandomAccessFile,但如果只需要读时可以使用FileInputStream,写映射文件时一定要使用随机访问文件,可能写时要读的原因吧。
该程序创建了一个128Mb的文件,如果一次性读到内存可能导致内存溢出,但这里访问好像只是一瞬间的事,这是因为,真正调入内存的只是其中的一小部分,其余部分则被放在交换文件上。这样你就可以很方便地修改超大型的文件了(最大可以到2 GB)。注意,Java是调用操作系统的”文件映射机制”来提升性能的。
RandomAccessFile类的应用:
/* * 程序功能:演示了RandomAccessFile类的操作,同时实现了一个文件复制操作。 */ package com.lwj.demo; import java.io.*; public class RandomAccessFileDemo { public static void main(String[] args) throws Exception { RandomAccessFile file = new RandomAccessFile("file", "rw"); // 以下向file文件中写数据 file.writeInt(20); // 占4个字节 file.writeDouble(8.236598); // 占8个字节 file.writeUTF("这是一个UTF字符串"); // 这个长度写在当前文件指针的前两个字节处,可用readShort()读取 file.writeBoolean(true); // 占1个字节 file.writeShort(395); // 占2个字节 file.writeLong(2325451l); // 占8个字节 file.writeUTF("又是一个UTF字符串"); file.writeFloat(35.5f); // 占4个字节 file.writeChar(‘a‘); // 占2个字节file.seek(0); // 把文件指针位置设置到文件起始处// 以下从file文件中读数据,要注意文件指针的位置 System.out.println("——————从file文件指定位置读数据——————"); System.out.println(file.readInt()); System.out.println(file.readDouble()); System.out.println(file.readUTF()); file.skipBytes(3); // 将文件指针跳过3个字节,本例中即跳过了一个boolean值和short值。 System.out.println(file.readLong()); file.skipBytes(file.readShort()); // 跳过文件中“又是一个UTF字符串”所占字节,注意readShort()方法会移动文件指针,所以不用加2。 System.out.println(file.readFloat()); //以下演示文件复制操作 System.out.println("——————文件复制(从file到fileCopy)——————"); file.seek(0); RandomAccessFile fileCopy=new RandomAccessFile("fileCopy","rw"); int len=(int)file.length(); //取得文件长度(字节数) byte[] b=new byte[len]; file.readFully(b); fileCopy.write(b); System.out.println("复制完成!"); } }

RandomAccessFile 插入写示例:
/** * * @param skip 跳过多少过字节进行插入数据 * @param str 要插入的字符串 * @param fileName 文件路径 */ public static void beiju(long skip, String str, String fileName){ try { RandomAccessFile raf = new RandomAccessFile(fileName,"rw"); if(skip < 0 || skip > raf.length()){ System.out.println("跳过字节数无效"); return; } byte[] b = str.getBytes(); raf.setLength(raf.length() + b.length); for(long i = raf.length() - 1; i > b.length + skip - 1; i--){ raf.seek(i - b.length); byte temp = raf.readByte(); raf.seek(i); raf.writeByte(temp); } raf.seek(skip); raf.write(b); raf.close(); } catch (Exception e) { e.printStackTrace(); } }

利用RandomAccessFile实现文件的多线程下载,即多线程下载一个文件时,将文件分成几块,每块用不同的线程进行下载。下面是一个利用多线程在写文件时的例子,其中预先分配文件所需要的空间,然后在所分配的空间中进行分块,然后写入:
import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; /** * 测试利用多线程进行文件的写操作 */ public class Test {public static void main(String[] args) throws Exception { // 预分配文件所占的磁盘空间,磁盘中会创建一个指定大小的文件 RandomAccessFile raf = new RandomAccessFile("D://abc.txt", "rw"); raf.setLength(1024*1024); // 预分配 1M 的文件空间 raf.close(); // 所要写入的文件内容 String s1 = "第一个字符串"; String s2 = "第二个字符串"; String s3 = "第三个字符串"; String s4 = "第四个字符串"; String s5 = "第五个字符串"; // 利用多线程同时写入一个文件 new FileWriteThread(1024*1,s1.getBytes()).start(); // 从文件的1024字节之后开始写入数据 new FileWriteThread(1024*2,s2.getBytes()).start(); // 从文件的2048字节之后开始写入数据 new FileWriteThread(1024*3,s3.getBytes()).start(); // 从文件的3072字节之后开始写入数据 new FileWriteThread(1024*4,s4.getBytes()).start(); // 从文件的4096字节之后开始写入数据 new FileWriteThread(1024*5,s5.getBytes()).start(); // 从文件的5120字节之后开始写入数据 }// 利用线程在文件的指定位置写入指定数据 static class FileWriteThread extends Thread{ private int skip; private byte[] content; public FileWriteThread(int skip,byte[] content){ this.skip = skip; this.content = content; }public void run(){ RandomAccessFile raf = null; try { raf = new RandomAccessFile("D://abc.txt", "rw"); raf.seek(skip); raf.write(content); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { raf.close(); } catch (Exception e) { } } } }}

MappedByteBuffer使用
其实掌握MappedByteBuffer并不难,只要记住“三方三法三特性”(我自己总结的,呵呵~~不要扔鸡蛋哦。。。)这句话就可以轻松搞定!MappedByteBuffer 只是一种特殊的 ByteBuffer ,即是ByteBuffer的子类。 MappedByteBuffer 将文件直接映射到内存(这里的内存指的是虚拟内存,并不是物理内存,后面说证明这一点)。通常,可以映射整个文件,如果文件比较大的话可以分段进行映射,只要指定文件的那个部分就可以。而且,与ByteBuffer十分类似,没有构造函数(你不可new MappedByteBuffer()来构造一个MappedByteBuffer),我们可以通过 Java.nio.channels.FileChannel 的 map() 方法来获取 MappedByteBuffer 。其实说的通俗一点就是Map把文件的内容被映像到计算机虚拟内存的一块区域,这样就可以直接操作内存当中的数据而无需操作的时候每次都通过I/O去物理硬盘读取文件,所以效率上有很大的提升!
三种方式:
FileChannel提供了map方法来把文件影射为内存映像文件: MappedByteBuffer map(int mode,long position,long size); 可以把文件的从position开始的size大小的区域映射为内存映像文件,mode指出了 可访问该内存映像文件的方式:READ_ONLY,READ_WRITE,PRIVATE.
  1. READ_ONLY,(只读): 试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException.(MapMode.READ_ONLY)
  2. READ_WRITE(读/写): 对得到的缓冲区的更改最终将传播到文件;该更改对映射到同一文件的其他程序不一定是可见的。 (MapMode.READ_WRITE)
  3. PRIVATE(专用): 对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其他程序也不是可见的;相反,会创建缓冲区已修改部分的专用副本。 (MapMode.PRIVATE)
三个方法
  1. fore(); 缓冲区是READ_WRITE模式下,此方法对缓冲区内容的修改强行写入文件
  2. load()将缓冲区的内容载入内存,并返回该缓冲区的引用
  3. isLoaded()如果缓冲区的内容在物理内存中,则返回真,否则返回假
三个特性
调用信道的map()方法后,即可将文件的某一部分或全部映射到内存中,映射内存缓冲区是个直接缓冲区,继承自ByteBuffer,但相对于ByteBuffer,它有更多的优点:
【(转) Java RandomAccessFile与MappedByteBuffer】a. 读取快
b. 写入快
c. 随时随地写入
 
MappedByteBuffer的读取/写入文件和普通I/O流的对比
import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; public class MapMemeryBuffer { public static void main(String[] args) throws Exception { ByteBuffer byteBuf = ByteBuffer.allocate(1024 * 14 * 1024); byte[] bbb = new byte[14 * 1024 * 1024]; FileInputStream fis = new FileInputStream("d://test"); FileOutputStream fos = new FileOutputStream("d://outFile.txt"); FileChannel fc = fis.getChannel(); long timeStar = System.currentTimeMillis(); //得到当前的时间fc.read(byteBuf); //1 读取long timeEnd = System.currentTimeMillis(); //得到当前的时间System.out.println("Read time :" + (timeEnd - timeStar) + "ms"); timeStar = System.currentTimeMillis(); fos.write(bbb); // 写入timeEnd = System.currentTimeMillis(); System.out.println("Write time :" + (timeEnd - timeStar) + "ms"); fos.flush(); fc.close(); fis.close(); } }

输出结果:
Read time :1874ms
Write time :360ms
把上面的程序的1换成MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fileLength);
换成mbb.flip();
输出结果:
Read ByteBuf take time :16ms
Write ByteBuf take time :0ms
可见普通I/O和MappedByteBuffer是没法比的。另外在写入的时候花了0ms说明Map写入机制是根据你的更改量来决定,就是只保存修改部分的!
参考:http://blog.csdn.net/mgoann/article/details/3345850
 
内存映射文件(memory-mapped file)能让你创建和修改那些大到无法读入内存的文件。
有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。这种解决思路能大大简化修改文件的代码。
import java.io.*; import java.nio.*; import java.nio.channels.*; public class LargeMappedFiles { static int length = 0x8FFFFFF; // 128 Mb public static void main(String[] args) throws Exception { MappedByteBuffer out = new RandomAccessFile("test.dat", "rw").getChannel() .map(FileChannel.MapMode.READ_WRITE, 0, length); for(int i = 0; i < length; i++) out.put((byte)‘x‘); System.out.println("Finished writing"); for(int i = length/2; i < length/2 + 6; i++) System.out.print((char)out.get(i)); } }

为了能以读写的方式打开文件,我们从RandomAccessFile入手。
拿到channel之后,我们用map( )方法生成了一个MappedByteBuffer。这是一种特殊的”direct buffer”。注意,你必须指明,它是从文件的哪个位置开始映射的,映射的范围又有多大;也就是说,它还可以映射一个大文件的某个小片断。
MappedByteBuffer是ByteBuffer的派生类,因此它具备了ByteBuffer的所有方法。这里只简单地演示了一下put( )和get( )方法,除此之外,你还可以使用asCharBuffer( )之类的方法。
上述例程创建了一个128MB的文件,或许这已经超出OS的允许范围了。文件的访问好像只是一瞬间的事,这是因为,真正调入内存的只是其中的一小部分,其余部分则被放在交换文件上。这样你就可以很方便地修改超大型的文件了(最大可以到2 GB)。
注意,Java是调用操作系统的”文件映射机制(file-mapping facility)”来提升性能的。
由于Java的文件锁是直接映射操作系统的锁机制的,因此其它进程也能看到文件锁。
虽然你可以用wrap( ) 直接把char数组转换成CharBuffer,但实际上它还是一个ByteBuffer,而CharBuffer只是它的view。由此可知,我们操控的对象永远都是ByteBuffer,因为只有它才能往channel里读写数据。
一般来说,你是不会让两个进程去共享一个网络socket的。)tryLock( ) 是非阻塞的。它会试着去获取这个锁,但是如果得不到(其它进程已经以独占方式得到这个锁了),那它就直接返回。而lock( )是阻塞的。如果得不到锁,它会在一直处于阻塞状态,除非它得到了锁,或者你打断了调用它(即lock( )方法)的线程,或者关闭了它要lock( )的channel,否则它是不会返回的。最后用FileLock.release( )释放锁。
还可以像这样锁住文件的某一部分
tryLock(long position, long size, boolean shared)
或者
lock(long position, long size, boolean shared)
这个方法能锁住文件的某个区域(size - position)。
其中第三个参数表示锁能不能共享。
虽 然在修改文件的过程中,无参数的lock( )和tryLock( )方法的锁定范围会随文件大小的变化,带参数的方法却不行。如果你锁住了position到position+size这段范围,而文件的长度又增加了, 那么position+size后面是不加锁的。而无参数的lock方法则会锁定整个文件,不管它变不变长。
锁是独占的还是共享的,这要由操作系统来决定。如果操作系统不支持共享锁,而程序又申请了一个,那么它会返回一个独占锁。你可以用FileLock.isShared( )来查询锁的类型(共享还是独占)。
 















    推荐阅读