先说结论
使用Java代码操作文件时,不管是读还是写,总是建议使用缓冲区。
开发者可以自己创建一个字节数组,每次read/write总是一块数据,而不是一个字节一个字节的读写。为此JDK直接提供了带缓冲区的输入输出流:BufferedInputStream、BufferedOutputStream。
先来看一下两者的性能对比,感受一下。
// 不带缓冲区
public static void main(String[] args) throws Exception {
FileOutputStream outputStream = new FileOutputStream("./a.txt");
long t1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
outputStream.write(1);
}
long t2 = System.currentTimeMillis();
System.out.println(t2 - t1);
}
// 带缓冲区
public static void main(String[] args) throws Exception {
FileOutputStream outputStream = new FileOutputStream("./a.txt");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
long t1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
bufferedOutputStream.write(1);
}
long t2 = System.currentTimeMillis();
System.out.println(t2 - t1);
}
向文件中写入一百万个’1’,程序耗时情况:
没有缓冲区 | 有缓冲区 |
---|---|
3299ms | 34ms |
可以看到,两者几乎是百倍的差距。
影响文件写入速度的因素
- 物理磁盘本身的读写速度
- 程序运行的中间损耗(用户态内核态的频繁切换)
为什么BufferedOutputStream写入速度那么快呢?
BufferedOutputStream
要想知道底层细节,必须看源码。
结论:
BufferedOutputStream通过内部维护的字节数组缓冲区,减少了大量的write()方法的底层调用。write先写缓冲区,缓冲区满再写磁盘。优点是速度很快,缺点是容易丢失数据。
write()方法为什么慢?
我们已经知道,BufferedOutputStream快的原因是因为减少了大量的write底层调用,那么,write()为什么那么慢呢???
如下代码,放到Linux上去运行。
死循环,每隔1秒写入一个字母【A】,使用strace监听程序的系统调用,这里为了方便写了个脚本。
#!/bin/bash
# 删除旧文件
rm -rf a.txt
rm -rf out.*
# 监听write、fsync事件、并输出到out文件
strace -e trace=write,fsync -ff -T -o out java Demo
结果:
再来看看BufferedOutputStream的系统调用情况,运行结果:
结论:
FileOutputStream的write()方法之所以慢,是因为每次执行都要触发Linux的系统调用,Linux的write系统调用需要OS从用户态切换到内核态。因为数据写入磁盘需要和硬件打交道,用户进程是运行在用户态上的,Linux出于安全考虑,用户态的进程是无法直接操作硬件的,需要通过系统调用交给内核来完成,由内核负责将数据写入到磁盘,而用户态到内核态的切换过程是比较重的且耗时,应该尽量避免。
write之后数据就持久化了吗?
write()的语义就是将数据写入到磁盘,那么是不是意味着,只要调用了write,数据就真的永久保存到磁盘了呢?
结论:NO!!!
页高速缓存 Page Cache
由于磁盘的IO速度实在是太慢了,和内存相比差好几个数量级,于是Linux系统就实现了一套高效的缓存机制:Page Cache。
Page Cache是基于内存来实现的一个针对磁盘IO优化的缓存机制,当内存充裕时,就尽可能的利用内存来减少磁盘IO的访问次数,以此来提升系统的整体性能。
读
当内核发起一个读请求时,首先会检查数据是否存在于Page Cache中,如果缓存命中,就直接返回,无需访问磁盘。否则从磁盘中读取数据并将其缓存到Page Cache中,这样后续的请求就可以命中缓存了。【访问过的数据下一次较高概率会再次访问】
写
当内核发起一个写请求时,同样是直接往Page Cache里写,OS会将该页置为脏的:【dirty】,并周期性的将脏页写回到磁盘,此时的写才是真正的写到物理磁盘里去了。还有一种情况是,内存不足了,OS必须淘汰掉一些页,在淘汰页之前,必须将脏页写回到磁盘,然后才能淘汰,释放内存。
综上所述,由于Page Cache的存在,write的数据并非就一定持久化到磁盘了,首先是被写入到Page Cache中,如果计算机突然断电,所有的脏页数据都会丢失,所以:高性能和数据一致性真的很难兼得。
如何保证数据一定写入物理磁盘?
连write都无法保证数据一定写入成功,如果程序就要追求严格的数据一致性,该如何处理呢???
例如:MySQL数据库,写入数据并commit之后,MySQL必须确保数据写入到磁盘,它是如何做到的呢?
fsync()
我们已经知道,write只是将数据写入到Page Cache中,至于OS什么时候将数据真正写入磁盘,我们完全无法预知,由OS自己决定。
但是,如果程序真的追求严格的数据一致性,Linux还提供了另一个函数:fsync()。
实际上,MySQL就是这么做的,当数据写入并commit之后,MySQL首先会将数据写入到redo log中,然后调用fsync()系统函数,将数据强制刷盘,这样就算系统宕机,MySQL也能重启后通过 redo log来恢复数据。
Java里如何实现呢?如下代码:
FileOutputStream outputStream = new FileOutputStream("./a.txt");
outputStream.write(1);
outputStream.getFD().sync();// 强制刷盘