文件读写性能受到诸多因素的影响,其中缓冲区是不容忽视的因素
下面看几个示例来比较一下各种文件操作的性能对比
1、单字节拷贝
将一个37M的文件test1.txt读出来然后写入到dest.txt,实现文件的拷贝。
示例代码:
@Test
public void perByteOperation() {
LOGGER.info("开始单字节单字节复制文件...");
long start = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(TEST_PATH + "test1.txt");
FileOutputStream fos = new FileOutputStream(TEST_PATH + "dest.txt")) {
int i;
while ((i = fis.read()) != -1) {
fos.write(i);
}
} catch (Exception e) {
LOGGER.error("单字节单字节复制文件异常", e);
}
LOGGER.info("结束单字节单字节复制文件...,耗时:{}ms", System.currentTimeMillis() - start);
}
[INFO ][2020/09/08 08:55:03][main] - 开始单字节单字节复制文件...
[INFO ][2020/09/08 08:57:31][main] - 结束单字节单字节复制文件...,耗时:148090ms
耗时148s,可怕!
2、采用100个字节的缓冲区
示例代码:
@Test
public void bufferOperationWith100() {
LOGGER.info("开始带100字节缓冲区复制文件...");
long start = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(TEST_PATH + "test1.txt");
FileOutputStream fos = new FileOutputStream(TEST_PATH + "dest.txt")) {
byte[] buf = new byte[100];
int len;
while ((len = fis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
} catch (Exception e) {
LOGGER.error("带100字节缓冲区复制文件", e);
}
LOGGER.info("开始带100字节缓冲区复制文件...,耗时:{}ms", System.currentTimeMillis() - start);
}
[INFO ][2020/09/08 08:59:05][main] - 开始带100字节缓冲区复制文件...
[INFO ][2020/09/08 08:59:07][main] - 开始带100字节缓冲区复制文件...,耗时:2237ms
可见,增加了缓冲区之后,速度大大提高了。
3、采用BufferedStream
BufferedStream采用了 private static int DEFAULT_BUFFER_SIZE = 8192; 8Kb的缓冲区。
示例代码:
@Test
public void bufferStreamByteOperation() {
LOGGER.info("开始带缓冲区复制文件...");
long start = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(TEST_PATH + "test1.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(TEST_PATH + "dest.txt"))) {
int i;
while ((i = bis.read()) != -1) {
bos.write(i);
}
} catch (Exception e) {
LOGGER.error("带缓冲区复制文件文件异常", e);
}
LOGGER.info("开始带缓冲区复制文件...,耗时:{}ms", System.currentTimeMillis() - start);
}
[INFO ][2020/09/08 09:01:18][main] - 开始带缓冲区复制文件...
[INFO ][2020/09/08 09:01:20][main] - 开始带缓冲区复制文件...,耗时:2350ms
为啥我们使用了缓冲流,没有明显提高呢?原因是虽然读取写入使用了缓冲流,但bos.write(i);方法是按单字节写入的,调用次数过多。
4、在BufferedStream基础上,写入的时候新增一个8KB的缓冲
@Test
public void bufferStreamBufferOperation() {
LOGGER.info("bufferStreamBufferOperation开始带缓冲区复制文件...");
long start = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(TEST_PATH + "test1.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(TEST_PATH + "dest.txt"))) {
byte[] buf = new byte[8196];
int i;
while ((i = bis.read(buf)) != -1) {
bos.write(buf, 0, i);
}
} catch (Exception e) {
LOGGER.error("带缓冲区复制文件文件异常", e);
}
LOGGER.info("开始带缓冲区复制文件...,耗时:{}ms", System.currentTimeMillis() - start);
}
[INFO ][2020/09/08 09:05:02][main] - bufferStreamBufferOperation开始带缓冲区复制文件...
[INFO ][2020/09/08 09:05:02][main] - 开始带缓冲区复制文件...,耗时:132ms
5、不采用缓冲流,只新增8KB的缓冲数组
@Test
public void streamBufferOperation() {
LOGGER.info("streamBufferOperation开始带缓冲区复制文件...");
long start = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(TEST_PATH + "test1.txt");
FileOutputStream fos = new FileOutputStream(TEST_PATH + "dest.txt")) {
byte[] buf = new byte[8196];
int i;
while ((i = fis.read(buf)) != -1) {
fos.write(buf, 0, i);
}
} catch (Exception e) {
LOGGER.error("带缓冲区复制文件文件异常", e);
}
LOGGER.info("开始带缓冲区复制文件...,耗时:{}ms", System.currentTimeMillis() - start);
}
[INFO ][2020/09/08 09:06:27][main] - streamBufferOperation开始带缓冲区复制文件...
[INFO ][2020/09/08 09:06:27][main] - 开始带缓冲区复制文件...,耗时:120ms
总结:
我们可以看到4,5花费的时间差不多,但我们推荐第4种方案。但在实际代码中每次需要读
取的字节数很可能不是固定的,有的时候读取几个字节,有的时候读取几百字节,这个时候有一个固定大小较大的缓冲,也就是使用 BufferedInputStream 和
BufferedOutputStream 做为后备的稳定的二次缓冲,就非常有意义了。
拓展:
DMA(直接内存访问),数据从磁盘经过总线直接发送到目标文件,无需经过内存和 CPU 进行数据中转,这种方式更快。
/**
* @Description 采用的是DMA(直接内存访问)也就是数据从磁盘经过总线直接发送到目标
* 文件,无需经过内存和 CPU 进行数据中转:
* @author chenwb
* @date 2020/9/8 9:21
*/
@Test
public void fileChannelOperation() throws IOException {
LOGGER.info("fileChannelOperation开始复制文件...");
long start = System.currentTimeMillis();
FileChannel in = FileChannel.open(Paths.get(TEST_PATH + "test1.txt"), StandardOpenOption.READ);
FileChannel out = FileChannel.open(Paths.get(TEST_PATH + "dest.txt"), StandardOpenOption.CREATE,
StandardOpenOption.WRITE);
in.transferTo(0, in.size(), out);
LOGGER.info("结束复制文件...,耗时:{}ms", System.currentTimeMillis() - start);
}
[INFO ][2020/09/08 09:22:30][main] - fileChannelOperation开始复制文件...
[INFO ][2020/09/08 09:22:30][main] - 结束复制文件...,耗时:48ms