Java NIO 内存映射文件

@author ixenos

文件操作的四大方法


 前提:内存的访问速度比磁盘高几个数量级,但是基本的IO操作是直接调用native方法获得驱动和磁盘交互的,IO速度限制在磁盘速度上

  由此,就有了缓存的思想,将磁盘内容预先缓存在内存上,这样当供大于求的时候IO速度基本就是以内存的访问速度为主,例如BufferedInput/OutputStream等

  而我们知道大多数OS都可以利用虚拟内存实现将一个文件或者文件的一部分映射到内存中,然后,这个文件就可以当作是内存数组一样地访问,我们可以把它看成一种“永久的缓存

  内存映射文件:内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件,此时就可以假定整个文件都放在内存中,而且可以完全把它当成非常大的数组来访问(随机访问)

  以下是四大文件操作对比

本图使用思维导图软件XMind制作

  在Core Java II中进行了这么一个实验:在同一台机器上,对JDK的jre/lib目录中的37MB的rt.jar文件分别用以上四种操作来计算CRC32校验和,记录下了如下时间

    方法     时间
  普通输入流     110s        
  带缓冲的输入流       9.9s
  随机访问文件     162s
  内存映射文件     7.2s

  这个小实验也验证了内存映射文件这个方法的可行性,由于具有随机访问的功能(映射在内存数组),所以常用来替代RandomAccessFile。

  当然,对于中等尺寸文件的顺序读入则没有必要使用内存映射以避免占用本就有限的I/O资源,这时应当使用带缓冲的输入流。

内存映射文件


  java.nio包使得内存映射变得十分简单

1、首先,从文件中获得一个通道(channel)通道是用于磁盘文件的一种抽象,它使我们可以访问诸如内存映射文件加锁机制(下文缓冲区数据结构部分将提到)文件间快速数据传递等操作系统特性。

 FileChannel channel = FileChannel.open(path, options);

  还能通过在一个打开的 File 对象(RandomAccessFile、FileInputStream 或 FileOutputStream)上调用 getChannel() 方法获取。调用 getChannel() 方法会返回一个连接到相同文件的 FileChannel 对象且该 FileChannel 对象具有与 File 对象相同的访问权限

2、然后,通过调用FileChannel类的map方法进行内存映射,map方法从这个通道中获得一个MappedByteBuffer对象(ByteBuffer的子类)

  你可以指定想要映射的文件区域与映射模式,支持的模式有3种:

  • FileChannel.MapMode.READ_ONLY:产生只读缓冲区,对缓冲区的写入操作将导致ReadOnlyBufferException;
  • FileChannel.MapMode.READ_WRITE:产生可写缓冲区,任何修改将在某个时刻写回到文件中,而这某个时刻是依赖OS的,其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于系统的,但是它是线程安全的
  • FileChannel.MapMode.PRIVATE:产生可写缓冲区,但任何修改是缓冲区私有的,不会回到文件中。。。
 import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.util.zip.*; public class MemoryMapTest
{ public static long checksumMappedFile(Path filename) throws IOException
{
//直接通过传入的Path打开文件通道
try (FileChannel channel = FileChannel.open(filename))
{
CRC32 crc = new CRC32();
int length = (int) channel.size();
//通过通道的map方法映射内存
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length); for (int p = 0; p < length; p++)
{
int c = buffer.get(p);
crc.update(c);
}
return crc.getValue();
}
} public static void main(String[] args) throws IOException
{
System.out.println("Mapped File:");
start = System.currentTimeMillis();
crcValue = checksumMappedFile(filename);
end = System.currentTimeMillis();
System.out.println(Long.toHexString(crcValue));
System.out.println((end - start) + " milliseconds");
}

3、一旦有了缓冲区,就可以使用ByteBuffer类和Buffer超类的方法来读写数据

  缓冲区支持顺序随机数据访问:

  顺序:有一个可以通过get和put操作来移动的位置

 while(buffer.hasRemaining()){
byte b = buffer.get(); //get当前位置
...
}

  随机:可以按内存数组索引访问

 for(int i=0; i<buffer.limit(); i++){
byte b = buffer.get(i); //这个get能指定索引
...
}

  可以用下面的方法来读写数据到一个字节数组(destination array):

  get(byte[] bytes) /get(byte[] bytes, int offset, int length)

    The method transfers bytes from this buffer into the given destination array.

  还有下列getXxx方法:getInt, getLong, getShort, getChar, getFloat, getDouble 用来读入在文件中存储为二进制值的基本类型值

    关于二进制数据排序机制不同的读取问题:

    我们知道,Java对二进制数据使用高位在前的排序机制(比如 0XA就是 0000 1010,高位在前低位在后),

    但是,如果需要低位在前的排序方式(0101 0000)处理二进制数字的文件,需调用:

    buffer.order(ByteOrder.LITTLE_ENDIAN);

    要查询缓冲区内当前的字节顺序,可以调用:

    ByteOrder b = buffer.order();

  要向缓冲区写数字,使用对应的putXxx方法,在恰当的时机,以及当通道关闭时,会将这些修改写回到文件中的哦

缓冲区数据结构


  在使用内存映射时,我们既可以创建单一的缓冲区横跨整个文件或者感兴趣的文件区域,也可以使用更多的缓冲区来读写大小适度的信息块。

  这一小节,就来讲讲缓冲区Buffer对象上的基本操作。

  缓冲区是具有相同基本类型的数值构成的数组(数组在内存中创建),Buffer类是一个抽象类,有以下具体的子类:ByteBuffer,CharBuffer,DoubleBuffer,IntBuffer,LongBuffer和ShortBuffer。(注意StringBuffer跟这些人没关系,而且String本质是引用类型)

  实践中,最常用的是ByteBuffer和CharBuffer。

  每个缓冲区都具有

    1、一个恒定的容量;

    2、一个读写位置,下一个值将在此进行读写;

    3、一个界限,超过他无法读写;

    4、一个可选的标记,用于重复一个读入或写出操作;

  0≤标记≤位置≤界限≤容量

  

  

  1、写:一开始时位置为0,界限等于容量,当我们不断调用put添值到缓冲区中,直至耗尽所有数据或者写出的数据集量达到容量大小时,就该进行读入操作了;

  2、读:这时调用 flip 方法将界限设置到当前位置(相当于trim),并把位置复位到0(为了读操作),现在在remaining方法返回(界限 — 位置)正数时,不断调用get;

   3、复位:将缓冲区中所有值读入后,调用clear(位置复位到0,界限复位到容量)使缓冲区为下一次循环做准备;

  4、复读:想复读缓冲区,可调用rewind或mark/reset方法;

  缓冲区的获得:

   A、内存映射时使用的是MappedByteBuffer,这是ByteBuffer的子类,由FileChannel的map()方法调用

  B、饿汉要获取缓冲区,可调用ByteBuffer.allocate或ByteBuffer.wrap这样的静态方法,然后用来自某个通道的数据填充缓冲区,或者将缓冲区的内容写出通道中:

 ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);

 //填充缓冲区
channel1.read(buffer);
//将Channel位置指定到newpos,作为覆盖文件内容的起点
channel1.position(newpos);
//将Buffer界限设置到当前位置,准备写出,注意区别Buffer和Channel的position,两者是不同的概念
buffer.flip();
//将缓冲区数据写出通道中
channel.write(buffer);

  这些方法和RandomAccessFile类的方法类似,但性能更高,因此常用以代替随机访问文件。

文件加锁机制


  线程安全:我们知道多线程并发修改共享数据会产生安全问题——竞争条件,为了保证对数据的原子性操作——同步存取,我们有了synchronized关键字添加隐式锁以及ReentranLock添加显式锁。但是多进程的同步存取又该怎么实现呢?

  进程安全:OS有个文件加锁机制,由于通道是对磁盘的一种抽象,FileChannel因此也实现了文件锁,可以调用其lock或tryLock方法进行锁定。

文件锁示例:锁定一个文件

 FileChannel channel = FileChannel.open(path);

 //调用lock,阻塞
FileLock lock = channel.lock(); //调用tryLock,立即响应
FileLock lock = channel.tryLock();

  1、第一个调用 lock() 会阻塞直至可获得锁,而第二个调用 tryLock() 将立即返回,要么获得锁,要么在锁不可获得的情况家返回null;

  2、这个文件将保持锁定状态,直至这个通道关闭,或者在锁上调用了release方法;

  3、还可以锁定文件的一部分:FileLock lock(long start, long size, boolean shared) 或 FileLock tryLock(long start, long size, boolean shared)

      a)如果shared标志位false,则锁定文件的目的是读写,而如果为true,则这是一个共享锁允许多个进程从文件中读入,并阻止任何进程获得独占的锁。调用FIleLock的isShared可查询当前持有的文件锁类型。

    b)如果锁了文件的尾部,但文件长度随后增长超过了锁定部分,那么超过的任然是不锁定的,此时需要使用 Long.MAX_VALUE 来表示尺寸。

   4、要确保在操作完成时释放锁,可用 try-with-resources 语句(FileLock实现了AutoCloseable接口)

 try(FileLock lock = channel.lock()){
...
}

  手动释放锁可调用FileLock对象的close()方法

注意点:

  1、文件加锁机制是依赖于操作系统的

  2、意外的建议锁:在某些系统中文件锁仅仅是建议性的,可能出现一个应用未能得到锁,它仍旧可以向被另一个进程并发锁定的文件执行写操作;

  3、意外的原子性:在某些系统中,不能在锁定一个文件的同时将其映射到内存中,原子性;

  4、意外的全释放:在某些系统中,关闭一个通道会释放由JVM持有的底层文件上的所有锁,因此避免在同一个锁定文件上使用多个通道,不然其他通道的锁也可能被释放!

  5、不可重入锁:文件锁是由整个JVM持有的,两个由同一VM启动的程序不可能获得在同一个文件上的锁,如果尝试对VM上已加锁的文件再加锁,将抛出OverlappingFileLockException;

    (注意:多线程的ReentranLock是可重入的!简称可重入锁,而文件锁是不可重入锁)

  6、在网络文件系统上锁定文件是高度依赖于系统的,尽量避免使用文件锁

Java NIO 内存映射文件的更多相关文章

  1. Java NIO内存映射---上G大文件处理(转)

    林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 摘要:本文主要讲了java中内存映射的原理及过程,与传统IO进行了对比,最后,用实例说明了结果 ...

  2. JAVA NIO 内存映射(转载)

    原文地址:http://blog.csdn.net/fcbayernmunchen/article/details/8635427     Java类库中的NIO包相对于IO 包来说有一个新功能是内存 ...

  3. Java利用内存映射文件实现按行读取文件

    我们知道内存映射文件读取是各种读取方式中速度最快的,但是内存映射文件读取的API里没有提供按行读取的方法,需要自己实现.下面就是我利用内存映射文件实现按行读取文件的方法,如有错误之处请指出,或者有更好 ...

  4. (转)NIO 内存映射文件

    内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多. 内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的.这其初听起来似乎 ...

  5. java 通过内存映射文件来提高IO读取文件性能

    MappedByteBuffer out = new RandomAccessFile("src/demo20/test.dat", "rw"). getCha ...

  6. 笔记:I/O流-内存映射文件

    内存映射文件时利用虚拟内存实现来将一个文件或者文件的一部分映射到内存中,然后整个文件就可以当作数组一样的访问,这个比传统的文件操作要快得多,Java 使用内存映射文件首先需要从文件中获取一个chann ...

  7. Java NIO之内存映射文件——MappedByteBuffer

    大多数操作系统都可以利用虚拟内存实现将一个文件或者文件的一部分"映射"到内存中.然后,这个文件就可以当作是内存数组来访问,这比传统的文件要快得多. 内存映射文件的一个关键优势是操作 ...

  8. 【JavaNIO的深入研究4】内存映射文件I/O,大文件读写操作,Java nio之MappedByteBuffer,高效文件/内存映射

    内存映射文件能让你创建和修改那些因为太大而无法放入内存的文件.有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问.这种解决办法能大大简化修改文件的代码.fileC ...

  9. JAVA NIO之浅谈内存映射文件原理与DirectMemory

    JAVA类库中的NIO包相对于IO 包来说有一个新功能是内存映射文件,日常编程中并不是经常用到,但是在处理大文件时是比较理想的提高效率的手段.本文我主要想结合操作系统中(OS)相关方面的知识介绍一下原 ...

随机推荐

  1. Pygame制作微信打飞机游戏PC版

    使用Pygame制作微信打飞机游戏PC版 转至:http://www.cnblogs.com/dukeleo/p/3339780.html   前一阵子看了一篇文章:青少年如何使用Python开始游戏 ...

  2. socket网络编程快速上手(二)——细节问题(4)

    5.慢系统调用及EINTR 还记得前面readn和writen函数么?里面有个EINTR,现在就来谈谈这个,这个很重要. Linux世界有个叫信号的东西,感觉他就像一位隐士,很少遇到他,而他又无处不在 ...

  3. Asp.net+jquery+ajaxpro异步仿Facebook纵向时间轴效果

    Asp.net+jquery+ajaxpro异步仿Facebook纵向时间轴效果 在一个项目中,用到了时间轴展示产品的开发进度,为了更好用户体验,想到了Facebook的timeline效果, 搜了一 ...

  4. 对C# 中Readonly的再认识

    C#中有两种常量类型,分别为readonly(运行时常量)与const(编译时常量),本文将就这两种类型的不同特性进行比较并说明各自的适用场景. 工作原理    readonly为运行时常量,程序运行 ...

  5. [C# 开发技巧]实现属于自己的截图工具

    [C# 开发技巧]实现属于自己的截图工具 一.引言 之前一直都是写一些C#基础知识的内容的,然而有些初学者可能看完了这些基础知识之后,会有这样一个疑惑的——我了解了这些基础知识之后,我想做一些工具怎么 ...

  6. 【Oracle】-【体系结构】-【DBWR】-DBWR进程相关理解

    对DBWR的一些理解 首先从名称上,DBWR全称是Database Writer Process,属于Oracle后台进程的一种,有的地方也叫DBWn,我想这里是出于DBWR进程个数的原因,DBWR进 ...

  7. 遍历Javascript数组的一种方法!

    应用场景: 如果您的数组只用一次的话就适用这种方法,因为遍历完后数组便清空了.代码如下: var arr=[1,5,6,2,3]; for(;i=arr.shift();){ console.log( ...

  8. 读书笔记:《HTML5开发手册》Web表单

    这是补充HTML5基础知识的第五篇内容,其他为: 一.HTML5-- 新的结构元素 二.HTML5-- figure.time.details.mark 三.HTML5-- details活学活用 四 ...

  9. “System.FormatException”类型的异常在 mscorlib.dll 中发生,但未在用户代码中进行处理 其他信息: 该字符串未被识别为有效的 DateTime。

    前台用过jquery ajax将值传递到 后台的一般处理程序 并且报了这个异常 原因是:前台传递过来的时间格式不对  需要把 "/"换成 "-"   例如:20 ...

  10. JavaScript :memory leak [转]

    Memory leak patterns in JavaScript Handling circular references in JavaScript applications Abhijeet ...