在java中,最难用的api非文件相关的api莫属。首先,在java文件相关api中划分了很多层,每一层又有很多功能和概念相同的api,不仔细区分和理解很容易混淆;其次,由于划分层次太细,导致api调用起来相当麻烦。
在文件读写时,可以划分为两大类:
- 字符相关的API一般都是以Writer关键词结尾的,这类文件就是平时我们能够读懂的文本文件,文本文件对人类友好,但涉及到内容的编码和解码,所以性能不如二进制好。
- 二进制相关的API一般都是以Stream结尾的,这类文件如图片、音乐、视频等字节文件,这类文件需要指定的编解码器才能处理,直接打开文件内容不能读懂,但二进制文件对机器友好,性能更高。
在操作系统底层,对于文件这类涉及到外部设备读写的操作,会涉及到内核态和用户态切换,这两种状态间切换会损耗很大的系统性能。所以系统底层就出现了零拷贝技术,它的核心思想就是将用户缓冲区和内核缓冲区建立映射关系,用户操作应用内存就是操作系统内存,避免了用户态和内核态之间的切换。
我们的应用大多部署在linux中,对于linux底层,实现零拷贝有两种方案,一种是mmap技术,另外一种是sendfile。对于mmap技术,适用于文件内容随时更改,建立内存映射后,操作内存数据就是操作文件数据;对于sendfile技术,常用于整个文件拷贝或发送场景。
接下来分别介绍java文件读写的几个常用api。
在介绍API时,先在类中定义相关内容如下:
public class TestFile {
/**
* 指定循环次数,这里是针对上面内容的循环次数,保证数据写入到文件中的数据是1G大小
*/
public static int COUNT = 0;
/**
* 模拟一个64字节字符串写入文件
*/
public static final String STR_CONTENT = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890|\n";
// public static final byte[] STR_BYTES = STR_CONTENT.getBytes(StandardCharsets.UTF_8);
/**
* 内存映射文件大小
*/
public static final int FILE_SIZE = 1073741824;
/**
* 定义内容大小的几个常量
*/
public static final int BYTES_64 = 64;
public static final int BYTES_1KB = 1024;
public static final int BYTES_8KB = 8 * 1024;
public static final int BYTES_64KB = 64 * 1024;
public static final int BYTES_512KB = 512 * 1024;
public static final int BYTES_4MB = 4 * 1024 * 1024;
public static final int BYTES_64MB = 64 * 1024 * 1024;
public static final int BYTES_256MB = 256 * 1024 * 1024;
public static final int BYTES_1GB = 1024 * 1024 * 1024;
public static void main(String[] args) {
// 每次写数据长度:要测试不同数据长度修改相关常量参数
int length = BYTES_64;
// 测试写文件类型:要测试不同的api修改参数类型即可
int type = 0;
// 拼接内容字符串
StringBuilder builder = new StringBuilder();
// 循环次数(每个字符串长度是64byte)
int loop = length / 64;
for(int i = 0; i < loop; ++i) {
builder.append(STR_CONTENT);
}
// 计算写文件循环次数
COUNT = FILE_SIZE / length;
// 获取写入文件字符串和字节数组
final String content = builder.toString();
final byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
// 写文件并获取执行耗时
final long start = System.currentTimeMillis();
switch (type) {
case 0:
writeFile0(content);
break;
case 1:
writeFile1(content);
break;
case 2:
writeFile2(content);
break;
case 3:
writeFile3(bytes);
break;
case 4:
writeFile4(bytes);
break;
case 5:
writeFile5(bytes);
break;
case 6:
writeFile6(bytes);
break;
}
System.out.println("write file use time : " + (System.currentTimeMillis() - start));
}
}
一、OutputStreamWriter 写文本文件:可以指定文件内容的编码方式,并且通过FileOutputStream构造方法的第二个参数指定文件内容是追加写入。
public static void writeFile0(String content) {
try (FileOutputStream os = new FileOutputStream("store/testfile0.txt", true);
OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8")) {
for(int i = 0; i < COUNT; i++) {
osw.write(content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
二、BufferedWriter:它实际上是在OutputStreamWriter基础上在包装了一层,增加了缓冲区,每次缓冲区满了之后将数据刷出到磁盘,其实在OutputStreamWriter和BufferedWriter里面有一个方法flush(),这个方法可以手动将数据刷出到磁盘
public static void writeFile1(String content) {
try (FileOutputStream fos = new FileOutputStream("store/testfile1.txt", true);
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
BufferedWriter bw = new BufferedWriter(osw)) {
for(int i = 0; i < COUNT; i++) {
bw.append(content);
}
} catch (Exception e) {
e.printStackTrace();
}
}
三、FileWriter、PrintWriter:这两个api也可以实现字符文件写入,api方法都是相通的,在执行文件写入时,性能不如BufferedWriter
public static void writeFile2(String content) {
try (FileWriter fw = new FileWriter("store/testfile2.txt", true)) {
for(int i = 0; i < COUNT; i++) {
fw.append(content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
四、FileOutputStream:这个api直接写二进制字节数组到文件中
public static void writeFile3(byte[] bytes) {
try (FileOutputStream os = new FileOutputStream("store/testfile3.txt", true)) {
for(int i = 0; i < COUNT; i++) {
os.write(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
}
五、BufferedOutputStream:它是对OutputStream的封装,增加了缓冲区加快文件写入速度
public static void writeFile4(byte[] bytes) {
try (FileOutputStream os = new FileOutputStream("store/testfile4.txt", true);
BufferedOutputStream bos = new BufferedOutputStream(os)) {
for(int i = 0; i < COUNT; i++) {
bos.write(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
}
下面分别介绍使用FileChannel方法和FileChannel中的map方法写入数据到文件:
FileChannel获取有以下几种方式:
# 通过FileOutputStream获取FileChannel,这种方式获取到的FileChannel只能写入数据
FileOutputStream fos = new FileOutputStream("store/testfile0.txt", true);
FileChannel channel = fos.getChannel();
# 通过FileInputStream获取FileChannel,这种方式获取到的FileChannel只能读取数据
FileInputStream fis = new FileInputStream("store/testfile0.txt");
FileChannel channel = fis.getChannel();
# 通过RandomAccessFile获取FileChannel,这种方式获取到的FileChannel既可以读也可以写数据
RandomAccessFile raf = new RandomAccessFile("store/testfile0.txt", "rw");
FileChannel channel = raf.getChannel();
# 通过FileChannel的open方法获取,这里可以指定打开方式,默认是只读方式打开
FileChannel channel = FileChannel.open(Paths.get("store/testfile0.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ);
六、FileChannel:通过FileOutputStream的getChannel方法获取到FileChannel管道,通过管道写数据到文件中
public static void writeFile5(byte[] bytes) {
try (FileOutputStream fos = new FileOutputStream("store/testfile5.txt", true);
FileChannel channel = fos.getChannel()) {
for(int i = 0; i < COUNT; i++) {
ByteBuffer wrap = ByteBuffer.wrap(bytes);
channel.write(wrap);
}
} catch (IOException e) {
e.printStackTrace();
}
}
七、mmap内存映射写文件:通过使用FileChannel的map方法将应用缓冲区与内核缓冲区建立映射关系,这时可以直接将数据写入到内核态,减少了数据拷贝
public static void writeFile6(byte[] bytes) {
try (RandomAccessFile raf = new RandomAccessFile("store/testfile6.txt", "rw");
FileChannel fc = raf.getChannel()) {
MappedByteBuffer mapper = fc.map(FileChannel.MapMode.READ_WRITE, 0, FILE_SIZE);
for(int i = 0; i < COUNT; i++) {
mapper.put(bytes);
}
mapper.force();
} catch (IOException e) {
e.printStackTrace();
}
}
下面的统计数据是在本地分别调用上面几个api写入1G文件不同内容大小的耗时(单位:毫秒):
使用API类型 | 64byte耗时 | 1kb耗时 | 8kb耗时 | 64kb耗时 | 512kb耗时 | 4mb耗时 | 64mb耗时 | 256mb耗时 |
---|---|---|---|---|---|---|---|---|
OutputStreamWriter | 2786 | 2527 | 2352 | 2400 | 2530 | 2572 | 3633 | 3457 |
BufferedWriter | 2469 | 2122 | 2032 | 2017 | 2083 | 2091 | 2327 | 2144 |
FileWriter | 2781 | 2599 | 2365 | 2422 | 2488 | 2695 | 3517 | 3527 |
FileOutputStream | 56625 | 4456 | 1511 | 1033 | 1038 | 1002 | 1281 | 1716 |
BufferedOutputStream | 2296 | 1674 | 1508 | 971 | 939 | 1006 | 1370 | 1648 |
FileChannel | 55131 | 4521 | 1528 | 965 | 973 | 1025 | 1277 | 1317 |
MappedByteBuffer | 2843 | 1953 | 1929 | 1941 | 1962 | 1902 | 2055 | 1996 |
对比上面api的调用耗时发现,FileOutputStream和FileChannel执行耗时非常高,其他几个API基本上相差不大,这是因为FileOutputStream每次调用write方法时就会将用户态数据刷出到内核态,然后持久化到磁盘文件中,其他api都会存在一个buffer缓冲区,当缓冲区满了时才会执行一次数据刷出,这个缓冲区默认大小是8kb,通过缓冲区技术可以减少用户态和内核态之间的切换,提高了系统的性能。
通过FileChannel写数据时,每次写入数据比较小时性能表现很差,当把每次写入数据量增大到几kb或几十kb时,会发现性能提升非常明显。
通过缓冲区技术可以减少状态转换,但是还是会涉及到将数据从用户内存写入到内核内存区域的过程,MappedByteBuffer写数据则用到了零拷贝技术,通过mmap技术实现内存映射,这时程序写内存直接写到内核缓冲区,操作系统会在合适时机将数据刷出到磁盘。
其实在rocketmq底层就有两种数据持久化方式,一种是使用mmap方式写,还有一种是使用DirectByteBuffer+FileChannel方式,并且使用异步方式刷盘,这样就会使写入文件性能非常高,但是该方式存在数据丢失风险,所以采用主从同步+异步持久化方案来保证高可用。
总结:通过以上api执行耗时对比,当写入小数据量时,通过缓冲区方案或内存映射方式写文件性能差异并不明显,但是当每次写入数据量比较大时,通过FileChannel写文件性能提升明显。如果在写入文件时又涉及到读取文件内容,这时选择MappedByteBuffer会是更加高明的方案。
对于文本文件读取常用的api如下:
一、BufferedReader:带有缓冲区的读取文件方法,与写缓冲区类似,都是为了加快文件读取速度。
public static void readFile0() {
InputStreamReader isr = null;
BufferedReader br = null;
try {
File file = new File("store/testfile0.txt");
if(file.exists()) {
isr = new InputStreamReader(new FileInputStream(file), "utf-8");
br = new BufferedReader(isr);
String line = br.readLine();
while (line != null) {
line = br.readLine();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(br != null) br.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if(isr != null) isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
二、Scanner:通过遍历方式读取文件,避免一次将文件都加载进内存导致内存溢出。
public static void readFile1() {
Scanner scanner = null;
try {
scanner = new Scanner(new File("store/testfile0.txt"));
String line = null;
while (scanner.hasNextLine()) {
line = scanner.nextLine();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if(scanner != null) scanner.close();
}
}
三、RandomAccessFile:通过RandomAccessFile也可以实现逐行读取数据。
public static void readFile2() {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile("store/testfile0.txt", "rw");
String line = raf.readLine();
while (line != null) {
line = raf.readLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(raf != null) raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
对于文本文件的读取还有其他相关的工具类做了非常好的封装,大家可以多了解一些,这里的方法都是基于jdk里面的相关api做的总结。
而二进制文件的读取涉及到解码,这里就不做过多的阐述,二进制文件可以通过RandomAccessFile相关的api或mmap内存映射来读取。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/181863.html