您当前的位置: 首页 >  nio

恐龙弟旺仔

暂无认证

  • 0浏览

    0关注

    282博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

NIO源码解析-FileChannel

恐龙弟旺仔 发布时间:2021-09-10 09:41:24 ,浏览量:0

前言:

FileChannel是一个连接到文件的通道,我们可以通过FileChannel来读写文件。在之前我们读写文件的方式中,主要是采用InputStream和OutputStream的流式方式来操作的。

本文会介绍下FileChannel的那些常用api。

1.FileChannel的基本结构

通过它的类结构图我们可以看到,FileChannel实现了对文件的读写操作,还被设置为可中断。下面来具体了解下其API。

2.FileChannel API 2.1 FileChannel的创建
File file = new File("D:\\test.txt");

// 1.通过RandomAccessFile创建
RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
FileChannel channel = raFile.getChannel();

// 2.通过FileInputStream创建
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel inputStreamChannel = fileInputStream.getChannel();

// 3.通过FileOutputStream创建
FileOutputStream fileOutputStream = new FileOutputStream(file);
FileChannel outputStreamChannel = fileOutputStream.getChannel();

 通过这三种方式创建的FileChannel有什么具体的区别呢?我们通过源码来比对下

// RandomAccessFile.getChannel()
channel = FileChannelImpl.open(fd, path, true, rw, this);

// FileInputStream.getChannel()
channel = FileChannelImpl.open(fd, path, true, false, this);

// FileOutputStream.getChannel()
channel = FileChannelImpl.open(fd, path, false, true, append, this);

// FileChannelImpl构造方法
private FileChannelImpl(FileDescriptor var1, String var2, boolean var3, boolean var4, boolean var5, Object var6) {
    this.fd = var1;
    this.readable = var3;
    this.writable = var4;
    this.append = var5;
    this.parent = var6;
    this.path = var2;
    this.nd = new FileDispatcherImpl(var5);
}

通过FileChannelImpl的私有构造方法我们可以了解到var3参数对应的是是否可读,var4对应的是是否可写。

再结合FileInputStream.getChannel FileOutputStream.getChannel时传入FileChannelImplement的参数,可以得到以下结果:

获取方式是否有文件读写权限RandomAccessFile.getChannel可读,是否可写根据传入mode来判断FileInputStream.getChannel可读,不可写FileOutputStream.getChannel可写,不可读

另:FileChannel还提供了一个open()的static方法,也可以通过该方式来获取,只不过这种方式不太常用,笔者不再详述。

2.2 RandomAccessFile的mode

RandomAccessFile的构造方法中有两个参数,分别对应file引用和mode(模式)。

mode具体有哪些值呢?我们直接看源码

public RandomAccessFile(File file, String mode)
    throws FileNotFoundException {
    
    String name = (file != null ? file.getPath() : null);
    int imode = -1;
    // read 只读模式
    if (mode.equals("r"))
        imode = O_RDONLY;
    // rw read and write 读写模式
    else if (mode.startsWith("rw")) {
        imode = O_RDWR;
        rw = true;
        if (mode.length() > 2) {
            // 还有s和d,分别对应于O_SYNC O_DSYNC
            if (mode.equals("rws"))
                imode |= O_SYNC;
            else if (mode.equals("rwd"))
                imode |= O_DSYNC;
            else
                imode = -1;
        }
    }
    ...
    fd = new FileDescriptor();
    fd.attach(this);
    path = name;
    open(name, imode);
}

O_SYNC O_DSYNC这两个分别代表什么呢?

笔者直接从网络上摘抄出来一段解释(来自https://zhuanlan.zhihu.com/p/104994838)

由于内存比磁盘读写速度快了好几个数量级,为了弥补磁盘IO性能低,Linux内核引入了页面高速缓存(PageCache)。我们通过Linux系统调用(open--->write)写文件时,内核会先将数据从用户态缓冲区拷贝到PageCache便直接返回成功,然后由内核按照一定的策略把脏页Flush到磁盘上,我们称之为write back。

write写入的数据是在内存的PageCache中的,一旦内核发生Crash或者机器Down掉,就会发生数据丢失,对于分布式存储来说,数据的可靠性是至关重要的,所以我们需要在write结束后,调用fsync或者fdatasync将数据持久化到磁盘上。
write back减少了磁盘的写入次数,但却降低了文件磁盘数据的更新速度,会有丢失更新数据的风险。为了保证磁盘文件数据和PageCache数据的一致性,Linux提供了sync、fsync、msync、fdatasync、sync_file_range5个函数。
open函数的O_SYNC和O_DSYNC参数有着和fsync及fdatasync类似的含义:使每次write都会阻塞到磁盘IO完成。

O_SYNC:使每次write操作阻塞等待磁盘IO完成,文件数据和文件属性都更新。
O_DSYNC:使每次write操作阻塞等待磁盘IO完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。

O_DSYNC和O_SYNC标志有微妙的区别:
文件以O_DSYNC标志打开时,仅当文件属性需要更新以反映文件数据变化(例如,更新文件大小以反映文件中包含了更多数据)时,标志才影响文件属性。在重写其现有的部分内容时,文件时间属性不会同步更新。
文件以O_SYNC标志打开时,数据和属性总是同步更新。对于该文件的每一次write都将在write返回前更新文件时间,这与是否改写现有字节或追加文件无关。相对于fsync/fdatasync,这样的设置不够灵活,应该很少使用。

实际上:Linux对O_SYNC、O_DSYNC做了相同处理,没有满足POSIX的要求,而是都实现了fdatasync的语义。

正是由于内存和磁盘之间的读写速度差异,所以才有了write方法只是将数据写入pageCache的优化做法,同时操作系统也提供了O_SYNC和O_DSYNC来保证数据刷入磁盘。

2.3 write写相关方法
// 1.将单个ByteBuffer写入FileChannel
public abstract int write(ByteBuffer src) throws IOException;

// 2.写入批量ByteBuffer,offset即ByteBuffer的offset
public abstract long write(ByteBuffer[] srcs, int offset, int length)
        throws IOException;

// 3.同2,offset为0
public final long write(ByteBuffer[] srcs) throws IOException {
    return write(srcs, 0, srcs.length);
}

标准写入方式:

File file = new File("D:\\test.txt");

// 1.通过RandomAccessFile创建
RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
FileChannel channel = raFile.getChannel();

ByteBuffer byteBuffer = ByteBuffer.allocate(100);

String text = "When grace is lost from life, come with a burst of song";
byteBuffer.put(text.getBytes());

byteBuffer.flip();
// 写入数据
while (byteBuffer.hasRemaining()) {
    channel.write(byteBuffer);
}

注意:write方法是在while循环中做的,因为无法保证一次write方法向FileChannel中写入多少字节

2.4 read读相关方法
// 1.将文件内容读取到单个ByteBuffer
public abstract int read(ByteBuffer dst) throws IOException;

// 2.将文件内容读取到ByteBuffer[]中,ByteBuffer的offset为指定值
public abstract long read(ByteBuffer[] dsts, int offset, int length)
        throws IOException;

// 3.同2
public final long read(ByteBuffer[] dsts) throws IOException {
    return read(dsts, 0, dsts.length);
}

标准读取方式:

File file = new File("D:\\test.txt");

// 1.通过RandomAccessFile创建
RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
FileChannel channel = raFile.getChannel();

ByteBuffer byteBuffer = ByteBuffer.allocate(100);
// 真正读取到readCount个字节
int readCount = channel.read(byteBuffer);

byteBuffer.flip();
byte[] array = byteBuffer.array();
// 将读取到的内容写入到String
String s = new String(array);
// 结果就是刚才2.3 write方法中写入的值
System.out.println(s);
2.5 force方法
public abstract void force(boolean metaData) throws IOException;

之前2.2说过,write方法写入文件可能只是写入了PageCache,如果此时系统崩溃,那么只存在于PageCache而没有刷入磁盘的数据就有可能丢失。使用force方法,我们就可以强制将文件内容和元数据信息(参数boolean metaData就是用来决定是否将元数据也写入磁盘)写入磁盘。该方法对一些关键性的操作,比如事务操作,就是非常关键的,使用force方法可以保证数据的完整性和可靠恢复。

2.6 lock相关方法
// 1.从file的position位置开始,锁定长度为size,锁定类别共享锁(true)或独占锁(false)
public abstract FileLock lock(long position, long size, boolean shared)
        throws IOException;

// 2.同1,基本独占全文件
public final FileLock lock() throws IOException {
    return lock(0L, Long.MAX_VALUE, false);
}

// 3.同1,尝试进行文件锁定
public abstract FileLock tryLock(long position, long size, boolean shared)
        throws IOException;

// 4.同2,尝试进行文件锁定
public final FileLock tryLock() throws IOException {
    return tryLock(0L, Long.MAX_VALUE, false);
}

首先,我们需要明白的是:锁定针对的是文件本身,而不是Channel或者线程。

FileLock可以是共享的,也可以是独占的。

锁的实现很大程度上依赖于本地的操作系统实现。当操作系统不支持共享锁时,则会主动升级共享锁为独占锁。

// 通过两个进程来测试下FileLock
FileLock lock = null;
try {
    File file = new File("D:\\test.txt");

    // 1.通过RandomAccessFile创建
    RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
    FileChannel channel = raFile.getChannel();

    // 主动设置独占锁或共享锁
    lock = channel.lock(0, Integer.MAX_VALUE, true);
    System.out.println(lock);
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        // 需要主动release
        lock.release();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
笔者基于使用Windows机器测试结果: 支持两个进程对同一文件的共享锁; 不支持两个进程对同一文件的独占锁(一个独占一个共享也不可以 )
总结:

本文主要介绍了下FileChannel的常用API。基于FileChannel,我们可以实现对文件的读写操作。

FileChannel还有些比较高级的API,比如map()、transferTo()、transferFrom()等,我们在下篇博客中继续介绍。

参考:

https://zhuanlan.zhihu.com/p/104994838

 

 

 

 

 

关注
打赏
1655041699
查看更多评论
立即登录/注册

微信扫码登录

0.0427s