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. ASP.NET MVC中使用Ninject

    ASP.NET MVC中使用Ninject 在[ASP.NET MVC 小牛之路]系列上一篇文章(依赖注入(DI)和Ninject)的末尾提到了在ASP.NET MVC中使用Ninject要做的两件事 ...

  2. Fortran使用隐形DO循环和reshape给一维和多维数组赋初值

    Fortran可以使用隐形DO循环和reshape给一维和多维数组赋初值. 下面以一维数组和二维数组为例,并给出程序结果: program main implicit none integer::i, ...

  3. 正则表达式引擎:nfa的转换规则。

    正则表达式引擎:nfa的转换规则. 正则到nfa 前言 在写代码的过程中,本来还想根据龙书上的说明来实现re到nfa的转换.可是写代码的时候发现,根据课本来会生成很多的无用过渡节点和空转换边,需要许多 ...

  4. Kendo UI开发教程(8): Kendo UI 特效概述

    Kendo UI Fx 提供了一个丰富,可扩展,性能经过优化的工具集合用来完成HTML元素的过渡显示.每种特效近可能的使用CSS Transition ,对于一些老版本浏览器使用修改属性的方法作为补充 ...

  5. How to install Savanna

    Pre-conditions: openstack has been installed successfully. 解压软件包中的savanna-all.tar.gz安装tar -C / -xzf ...

  6. AS3中释放优化的几条常识

    as3中垃圾和堆弃物如不及时清理,会造成进程的速度方面授予限制,下面讲几点关于释放优化的几条内容. 被删除对象在外部的所有引用一定要被删除干净才能被系统当成垃圾回收处理掉: 父对象内部的子对象被外部其 ...

  7. Haxe2.10到Haxe3,NME到OpenFL的迁移备忘

    终于决定正式向Haxe3和OpenFL迁移了,这期间也遇到不少问题,这里总结记录如下: 首先是Haxe3环境 * 因为还想保留Haxe 2.10的环境,因此没有使用官网的Haxe 3安装包,而是下载了 ...

  8. javaSocket与C通信

    前段时间写了个web端与C服务端之间的通信不过用的是短连接 非堵塞的方式,一直想使用长连接,使tomcat启动的时候就和C服务端进行通信,但是一直没找到方法希望je的朋友能给点思路.先来看我现在的具体 ...

  9. 筛选实现C++实现筛选法

    每日一贴,今天的内容关键字为筛选实现 筛选法 分析: 筛选法又称筛法,是求不超越自然数N(N>1)的全部质数的一种方法.据说是古希腊的埃拉托斯特尼(Eratosthenes,约公元前274-19 ...

  10. java.lang.ClassNotFoundException: [Ljava.lang.String解决办法

    原来jdk5.0的时候不会报这个错,用了jdk6.0就出现了这个错误,因为没有重载java.lang.String这个类 解决方法: 在vm缺省参数里添加-Dsun.lang.ClassLoader. ...