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的modeRandomAccessFile的构造方法中有两个参数,分别对应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();
}
}
本文主要介绍了下FileChannel的常用API。基于FileChannel,我们可以实现对文件的读写操作。
FileChannel还有些比较高级的API,比如map()、transferTo()、transferFrom()等,我们在下篇博客中继续介绍。
参考:https://zhuanlan.zhihu.com/p/104994838