FileChannel是Java NIO库中的一个类,用于对文件进行读写操作。它提供了一种高效的方式来读取、写入和操作文件。
使用FileChannel,你可以执行以下操作:
- 从文件读取数据到缓冲区(Buffer):你可以使用FileChannel的read()方法将数据从文件读取到缓冲区中。
- 将数据从缓冲区写入到文件:你可以使用FileChannel的write()方法将数据从缓冲区写入到文件中。
- 文件位置操作:你可以使用FileChannel的position()方法获取或设置文件的当前位置。
- 文件截取操作:你可以使用FileChannel的truncate()方法截取文件的大小。
- 强制数据同步到磁盘:你可以使用FileChannel的force()方法将数据强制刷新到磁盘上。
文件的顺序读写
要使用FileChannel,首先需要通过FileInputStream或FileOutputStream获取一个FileChannel实例,然后可以使用该实例进行文件的读写操作。
下面是一个简单的示例代码,展示了如何使用FileChannel读取文件内容并写入到另一个文件中:
package com.morris.io;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
/**
* FileChannel实现随机读取文件
*/
public class RandomAccessDemo {
public static void main(String[] args) throws IOException {
// 创建文件对象和FileChannel对象
File file = new File("RandomAccessDemo.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
FileChannel fileChannel = randomAccessFile.getChannel();
// 设置文件的当前位置
long readPosition = 10;
fileChannel.position(readPosition);
// 创建缓冲区
ByteBuffer readBuffer = ByteBuffer.allocate(5);
// 从当前位置读取数据到缓冲区
int bytesRead = fileChannel.read(readBuffer);
System.out.println(new String(readBuffer.array()));
long writePosition = 20; // 这个位置文件中没数据
fileChannel.position(writePosition);
// 创建缓冲区,一个char两个字节
ByteBuffer writeBuffer = ByteBuffer.allocate(6);
writeBuffer.put("xyz".getBytes(StandardCharsets.UTF_8));
writeBuffer.flip();
fileChannel.write(writeBuffer);
// 关闭通道和文件
fileChannel.close();
randomAccessFile.close();
}
}
产生的系统调用如下:
openat(AT_FDCWD, "input.txt", O_RDONLY) = 4
fstat(4, {st_mode=S_IFREG|0777, st_size=13, ...}) = 0
openat(AT_FDCWD, "output.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 7
fstat(7, {st_mode=S_IFREG|0777, st_size=0, ...}) = 0
read(4, "abc\nooxx\nefg\n", 1024) = 13
write(7, "abc\nooxx\nefg\n", 13) = 13
read(4, "", 1024) = 0
close(4)
close(7)
文件的随机读写
FileChannel提供了随机读写文件的功能,可以通过position()方法来设置文件的当前位置,然后使用read()方法从该位置开始读取数据,使用write()方法从该位置开始写入数据。
下面是一个示例代码,展示了如何使用FileChannel进行随机读写文件的操作:
package com.morris.io;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
/**
* FileChannel实现随机读取文件
*/
public class RandomAccessDemo {
public static void main(String[] args) throws IOException {
// 创建文件对象和FileChannel对象
File file = new File("RandomAccessDemo.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
FileChannel fileChannel = randomAccessFile.getChannel();
// 设置文件的当前位置
long readPosition = 10;
fileChannel.position(readPosition);
// 创建缓冲区
ByteBuffer readBuffer = ByteBuffer.allocate(5);
// 从当前位置读取数据到缓冲区
int bytesRead = fileChannel.read(readBuffer);
System.out.println(new String(readBuffer.array()));
long writePosition = 20;
fileChannel.position(writePosition);
// 创建缓冲区,一个char两个字节
ByteBuffer writeBuffer = ByteBuffer.allocate(6);
writeBuffer.put("xyz".getBytes(StandardCharsets.UTF_8));
writeBuffer.flip();
fileChannel.write(writeBuffer);
// 关闭通道和文件
fileChannel.close();
randomAccessFile.close();
}
}
产生的系统调用如下:
openat(AT_FDCWD, "RandomAccessFileDemo.txt", O_RDWR|O_CREAT, 0666) = 4
fstat(4, {st_mode=S_IFREG|0644, st_size=18, ...}) = 0
lseek(4, 10, SEEK_SET) = 10
read(4, "j\nklm", 5) = 5
write(1, "j\nklm\n", 6) = 6
lseek(4, 20, SEEK_SET) = 20
write(4, "xyz", 3) = 3
close(4)
系统调用使用lseek来移动定位position在文件中的位置。
如果访问文件的position大于文件的长度会怎么样?
程序执行前的文件内容:
abcde
fghij
klmno
程序执行后的文件内容:
abcde
fghij
klmno
^@^@xyz
可以看到访问文件的position大于文件的长度后,中间会用空来填充。
内存映射文件mmap
FileChannel还提供了内存映射文件的功能,通过使用map()方法,可以将文件映射到内存中的一个ByteBuffer对象,从而实现对文件的高效读写操作。
下面是一个示例代码,展示了如何使用内存映射文件的方式读取和写入文件:
package com.morris.io;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* mmap,将文件映射到内存中
*/
public class MemoryMappedFileDemo {
public static void main(String[] args) throws IOException {
// 创建文件对象和FileChannel对象
File file = new File("MemoryMappedFileDemo.txt");
FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
// 将文件映射到内存中
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
// 读取文件内容
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get()); // 输出数据
}
System.in.read();
// 修改文件内容
buffer.put(0, (byte) 'H');
buffer.put(1, (byte) 'e');
buffer.put(2, (byte) 'l');
buffer.put(3, (byte) 'l');
buffer.put(4, (byte) 'o');
// 刷新缓冲区到磁盘
buffer.force();
// 关闭通道和文件
fileChannel.close();
}
}
产生的系统调用如下:
openat(AT_FDCWD, "MemoryMappedFileDemo.txt", O_RDWR|O_CREAT, 0666) = 4
fstat(4, {st_mode=S_IFREG|0777, st_size=18, ...}) = 0
mmap(NULL, 18, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x7f4fa4004000
write(1, "a", 1) = 1
write(1, "b", 1) = 1
write(1, "c", 1) = 1
write(1, "d", 1) = 1
write(1, "e", 1) = 1
write(1, "\n", 1) = 1
write(1, "f", 1) = 1
write(1, "g", 1) = 1
write(1, "h", 1) = 1
write(1, "i", 1) = 1
write(1, "j", 1) = 1
write(1, "\n", 1) = 1
write(1, "k", 1) = 1
write(1, "l", 1) = 1
write(1, "m", 1) = 1
write(1, "n", 1) = 1
write(1, "o", 1) = 1
write(1, "\n", 1) = 1
read(0, "\n", 8192)
msync(0x7f4fa4004000, 18, MS_SYNC) = 0
close(4)
中间的代码System.in.read()
可以让程序暂停,这时可以查看文件打开的描述符:
$ lsof -p 8964
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 8964 root mem REG 0,50 18 10977524091816630 /io-demo/target/classes/MemoryMappedFileDemo.txt
java 8964 root 4u REG 0,50 18 10977524091816630 /io-demo/target/classes/MemoryMappedFileDemo.txt
将文件映射到内存中可以指定长度,这样可以做到只映射文件的部分内容,如果映射的长度大于文件本身的长度,就会扩大文件的长度。这样就限制了文件无法完成拓展,因为mmap到内存的时候,所能操作的范围就确定了,无法增加文件的长度。
mmap的优点:
- 高效访问:mmap使得文件的读写操作像访问内存一样高效,避免了频繁的系统调用和数据拷贝。
- 文件共享:多个进程可以将同一个文件映射到各自的地址空间,实现文件共享,方便进程间通信和数据共享。
- 零拷贝:与零拷贝技术结合,可以在网络传输中减少数据拷贝,提高传输性能。
- 内存管理:支持用户空间的内存管理,例如内存映射和私有化。
FileChannel锁定文件
FileChannel还提供了文件锁定的功能,可以通过lock()方法来对文件进行加锁,以防止其他进程对文件的读写操作。
下面是一个示例代码,展示了如何使用文件锁定的方式对文件进行操作:
package com.morris.io;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
/**
* FileChannel可以对文件加锁
*/
public class FileLockDemo {
public static void main(String[] args) throws IOException {
// 创建文件对象和FileChannel对象
File file = new File("FileLockDemo.txt");
FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
// 对文件进行加锁
FileLock lock = fileChannel.lock();
new Thread(() -> {
try {
main(args);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 暂停下,让其他线程访问下文件,演示锁的效果
System.in.read();
// 执行文件操作(读取、写入等)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("file lock".getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
fileChannel.write(byteBuffer);
// 解锁文件
lock.release();
// 关闭通道和文件
fileChannel.close();
}
}
产生的系统调用如下:
openat(AT_FDCWD, "FileLockDemo.txt", O_RDWR|O_CREAT, 0666) = 4
fstat(4, {st_mode=S_IFREG|0777, st_size=0, ...}) = 0
fcntl(4, F_SETLKW, {l_type=F_WRLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
read(0, "\n", 8192)
write(4, "file lock", 9) = 9
fcntl(4, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
close(4)
中间的代码System.in.read()
可以让程序暂停,这时可以查看文件打开的描述符:
java 9355 root 4uW REG 0,50 12 14073748835656455 /io-demo/target/classes/FileLockDemo.txt
可以看到文件描述符4
后面多了个W
。
其他线程访问已加锁的文件会抛出异常:
Exception in thread "Thread-0" java.nio.channels.OverlappingFileLockException
at java.base/sun.nio.ch.FileLockTable.checkList(FileLockTable.java:229)
at java.base/sun.nio.ch.FileLockTable.add(FileLockTable.java:123)
at java.base/sun.nio.ch.FileChannelImpl.lock(FileChannelImpl.java:1276)
at java.base/java.nio.channels.FileChannel.lock(FileChannel.java:1089)
at com.morris.io.FileLockDemo.main(FileLockDemo.java:22)
at com.morris.io.FileLockDemo.lambda$main$0(FileLockDemo.java:26)
at java.base/java.lang.Thread.run(Thread.java:833)
但是其他进程访问这个已经加锁的文件,可以成功,执行如下的命令可以成功,why???
$ echo xxx > FileLockDemo.txt