从Linux底层理解为什么带Buffer的输出流数据写入速度更快


先说结论

使用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’,程序耗时情况:

没有缓冲区有缓冲区
3299ms34ms

可以看到,两者几乎是百倍的差距。

影响文件写入速度的因素

  • 物理磁盘本身的读写速度
  • 程序运行的中间损耗(用户态内核态的频繁切换)

为什么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();// 强制刷盘
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值