NIO 之 FileChannel

概述

文件通道总是阻塞式的,因此不能被置于非阻塞模式。现代操作系统都有复杂的缓存和预取机制,使得本地磁盘 I/O 操作延迟很少。网络文件系统一般而言延迟会多些,不过却也因该优化而受益。 面向流的 I/O 的非阻塞范例对于面向文件的操作并无多大意义,这是由文件 I/O 本质上的不同性质造成的。对于文件 I/O,最强大之处在于异步 I/O( asynchronous I/O),它允许一个进程可以从操作系统请求一个或多个 I/O 操作而不必等待这些操作的完成。发起请求的进程之后会收到它请求的 I/O 操作已完成的通知。

异步 I/O 是在JDK 1.7 才被加入的 java.nio.channels.AsynchronousFileChannel 。

FileChannel

FileChannel对象不能直接创建。一个FileChannel实例只能通过在一个打开的file对象( RandomAccessFile、 FileInputStream或 FileOutputStream)上调用getChannel( )方法
获取。调用getChannel( )方法会返回一个连接到相同文件的FileChannel对象且该FileChannel对象具有与file对象相同的访问权限,然后您就可以使用该通道对象来利用强大的FileChannel API了。

FileChannel 线程安全

FileChannel 是线程安全的类,支持多个线程同时并发访问,但不是所有的方法都能多线程同时并发访问,比如,文件大小,file postion 等,该方法要想获取正确的值,只能加锁访问。

FileChannel 类结构

public abstract class FileChannel extends AbstractInterruptibleChannel
        implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {
    protected FileChannel() {}

    public static FileChannel open(Path path, OpenOption... options) throws IOException {}

    public abstract int read(ByteBuffer dst) throws IOException;

    public abstract int write(ByteBuffer src) throws IOException;

    public abstract long position() throws IOException;

    public abstract FileChannel position(long newPosition) throws IOException;

    public abstract long size() throws IOException;

    public abstract FileChannel truncate(long size) throws IOException;

    public abstract void force(boolean metaData) throws IOException;

    public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;

    public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;

    public abstract int read(ByteBuffer dst, long position) throws IOException;

    public abstract int write(ByteBuffer src, long position) throws IOException;

    public static class MapMode {
        public static final MapMode READ_ONLY = new MapMode("READ_ONLY");
        public static final MapMode READ_WRITE = new MapMode("READ_WRITE");
        public static final MapMode PRIVATE = new MapMode("PRIVATE");
    }

    public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;

    public abstract FileLock lock(long position, long size, boolean shared) throws IOException;

    public final FileLock lock() throws IOException {}

    public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;

    public final FileLock tryLock() throws IOException {}

}

open() 方法

public static FileChannel open(Path path, Set<? extends OpenOption> options,
           FileAttribute<?>... attrs) throws IOException {}
public static FileChannel open(Path path, OpenOption... options) throws IOException {}

position() 方法

每个 FileChannel 都有一个叫“file position”的概念。这个 position 值决定文件中哪一处的数据接下来将被读或者写。

FileChannel 类为我们提供了两种position( )方法。

public abstract long position() throws IOException;
public abstract FileChannel position(long newPosition) throws IOException;
  • 第一种,不带参数的,返回当前文件的position值。返回值是一个长整型( long),表示文件中的当前字节位置
  • 第二种形式的 position( )方法带一个 long(长整型) 参数并将通道的 position 设置为指定值。

position 必须是大于等于0的整数。但是 postion 的大小可以超出文件的大小。

当 position 的位置大于文件的长度时分以下两种情况:

  1. 调用 read() 方法,无法读取的数据,相当于读取到文件末尾。
  2. 调用 write() 方法,会在当前position的位置写入缓冲区中的字节。写方法可能会引起产生文件空洞。

文件空洞

当磁盘上一个文件的分配空间小于它的文件大小时会出现“文件空洞”。对于内容稀疏的文件,大多数现代文件系统只为实际写入的数据分配磁盘空间(更准确地说,只为那些写入数据的文件系统页分配空间)。假如数据被写入到文件中非连续的位置上,这将导致文件出现在逻辑上不包含数据的区域( 即“空洞”)。

例如:文件大小为 10 byte,现在把position 设置为100,然后调用 write 方法写入10个字节,现在的文件大小为 110 字节。而文件系统为了优化而磁盘中实际只占用了20字节,其它90个字节未分配空间。当真正写入的时候才分配磁盘空间。
是否产生文件空洞,取决与文件系统的实现。

truncate() 方法

当需要减少一个文件的 size 时, truncate( )方法会砍掉您所指定的新 size 值之外的所有数据。如果 truncate 的 size 大于文件的 size 该文件不会修改。如果truncate 的 size 小于或等于当前的文件 size 值,该文件会把 truncate 的 size 后面的数据删掉。如果postion的值大于 truncate size 的值,在 truncate 后会把 postion的值修改为 truncate size 的值。

    public static void main(String[] args) throws Exception {
        RandomAccessFile fis = new RandomAccessFile(new File("d:\\a.txt"),"rw");
        FileChannel fs =fis.getChannel();
        ByteBuffer bb = ByteBuffer.allocate(10);
        fs.position(100);
        fs.write(bb);
        fs.position(1000);
        System.out.println("原始size:"+fs.size() + "\tposition:"+fs.position());
        fs.truncate(200);
        System.out.println("truncate 200\tsize:"+fs.size()+ "\tposition:"+fs.position());
        
        fs.truncate(100);
        System.out.println("truncate 100\tsize:"+fs.size()+ "\tposition:"+fs.position());
    }
原始size:110  position:1000
truncate 200    size:110    position:200
truncate 100    size:100    position:100

force() 方法

该方法告诉 FileChannel,强制把所有修改的数据全部写如到磁盘上。
文件系统可能为了性能,把要修改的数据先写入缓存,等缓存写满后一块同步写入到磁盘中,使用缓存来提高文件的读写速度。

transferTo() 和 transferFrom() 方法

transferTo( )和 transferFrom( )方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据。只有 FileChannel 类有这两个方法,因此 channel-to-channel 传输中通道之一必须是 FileChannel。您不能在 socket 通道之间直接传输数据,不过 socket 通道实现WritableByteChannel 和 ReadableByteChannel 接口,因此文件的内容可以用 transferTo( )方法传输给一个 socket 通道,或者也可以用 transferFrom( )方法将数据从一个 socket 通道直接读取到一个文件中。

直接的通道传输不会更新与某个 FileChannel 关联的 position 值。请求的数据传输将从position 参数指定的位置开始,传输的字节数不超过 count 参数的值。实际传输的字节数会由方法返回,可能少于您请求的字节数。

对于传输数据来源是一个文件的 transferTo( )方法,如果 position + count 的值大于文件
的 size 值,传输会在文件尾的位置终止。假如传输的目的地是一个非阻塞模式的 socket 通道,那么当发送队列( send queue) 满了之后传输就可能终止,并且如果输出队列( output queue)已满的话可能不会发送任何数据。类似地,对于 transferFrom( )方法:如果来源 src 是另外一个 FileChannel并且已经到达文件尾,那么传输将提早终止;如果来源 src 是一个非阻塞 socket 通道,只有当前处于队列中的数据才会被传输(可能没有数据)。由于网络数据传输的非确定性,阻塞模式的socket 也可能会执行部分传输,这取决于操作系统。许多通道实现都是提供它们当前队列中已有的数据而不是等待您请求的全部数据都准备好。

注意:
NIO,非阻塞通道,不要使用 transferTo 或 tranferFrom 来传输数据,传输的数据可能会不完整。

map() 方法

map( )的方法,该方法可以在一个打开的文件和一个特殊类型的 ByteBuffer 之间建立一个虚拟内存映射。在 FileChannel 上调用 map( )方法会创建一个由磁盘文件支持的虚拟内存映射( virtual memory mapping)并在那块虚拟内存空间外部封装一个 MappedByteBuffer 对象。

由 map( )方法返回的 MappedByteBuffer 对象(直接内存)的行为在多数方面类似一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘上的一个文件中。调用 get( )方法会从磁盘文件中获取数据。通过文件映射看到的数据同您用常规方法读取文件看到的内容是完全一样的。相似地,对映射的缓冲区实现一个 put( )会更新磁盘上的那个文件,并且您做的修改对于该文件的其他阅读者也是可见的。

通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高。因为不需要做明确的系统调用,那会很消耗时间。更重要的是,操作系统的虚拟内存可以自动缓存内存页( memory page)。这些页是用系统内存来缓存的,所以不会消耗 Java 虚拟机内存堆( memory heap)。

lock() 方法

调用带参数的 Lock( )方法会指定文件内部锁定区域的开始 position 以及锁定区域的 size。第三个参数 shared 表示您想获取的锁是共享的(参数值为 true)还是独占的(参数值为 false)。要获得一个共享锁,您必须先以只读权限打开文件,而请求独占锁时则需要写权限。另外,您提供的 position和 size 参数的值不能是负数。

FileChannel 的 lock 支持获取共享锁和独占锁(lock 方法的第三个参赛 shared)。是否支持共享锁还得依赖本地的操作系统实现。并非所有的操作系统和文件系统都支持共享文件锁。对于那些不支持的,对一个共享锁的请求会被自动提升为对独占锁的请求。这可以保证准确性却可能严重影响性能。

锁的对象是文件而不是通道或线程,如果在同一个进程使用多线程获取文件锁,只要一个能获取到锁,那么其它的所遇咸菜都可以获取到锁。

锁定区域的范围不一定要限制在文件的 size 值以内,锁可以扩展从而超出文件尾。因此,我们可以提前把待写入数据的区域锁定,我们也可以锁定一个不包含任何文件内容的区域,比如文件最后一个字节以外的区域。如果之后文件增长到达那块区域,那么您的文件锁就可以保护该区域的文件内容了。相反地,如果您锁定了文件的某一块区域,然后文件增长超出了那块区域,那么新增加的文件内容将不会受到您的文件锁的保护。

不带参数的 lock() 方法,默认获取的是独占锁,并且锁定的文件区域是 0 到 Long.MAX_VALUE。

public final FileLock lock() throws IOException {
    return lock(0L, Long.MAX_VALUE, false);
}

文件锁使用,详见文章:JAVA 文件锁 FileLock

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容

  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    JackChen1024阅读 7,520评论 1 143
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    zhisheng_blog阅读 1,109评论 0 7
  • (转载说明:本文非原创,转载自http://ifeve.com/java-nio-all/) Java NIO: ...
    数独题阅读 799评论 0 3
  • (转载说明:本文非原创,转载自http://ifeve.com/java-nio-all/) Java NIO: ...
    柳岸阅读 813评论 0 3
  • 岁末年初是一年中最忙的时候,在冬日凛冽的寒风里穿梭于单位和家,日复一日,感觉自己像疲于奔命的小陀螺。多久没有好好放...
    水清浅的札记阅读 370评论 2 1