NIO 之 FileChannel

作者: jijs | 来源:发表于2018-05-26 23:52 被阅读107次

概述

文件通道总是阻塞式的,因此不能被置于非阻塞模式。现代操作系统都有复杂的缓存和预取机制,使得本地磁盘 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

相关文章

网友评论

  • 黄云斌huangyunbin:NIO,非阻塞通道,不要使用 transferTo 或 tranferFrom 来传输数据,传输的数据可能会不完整
    -----------为什么呢?

    异步 I/O 是在JDK 1.7 才被加入的 java.nio.channels.AsynchronousFileChannel
    -------------异步文件io能举个例子吗

本文标题:NIO 之 FileChannel

本文链接:https://www.haomeiwen.com/subject/wthjrftx.html