输入输出流的缓冲区设置多大比较合适

刚刚在写代码,需要把一个文件读进来,然后压缩后写出去,在读取文件的时候,源代码如下:

val array = ByteArray(1024)
var len: Int
while (inputStream.read(array).also { len = it } != -1) {
	zipOutputStream.write(array, 0, len)
}

这里使用的是Kotlin语言,跟Java差不了多少,我们从inputStream中读取字节,将读取到的字节存储在array数组中,这里我定义的数组大小为1024,此时我突然想到一个问题,这个大小设置多少合适?如果设置的太小肯定不好,会导致多次访问文件,想到这里我就又想到JDK有提供一个BufferedInpuStream,用于提升读取的效率,这时我在想缓冲流不就是提供了一个缓冲区吗?如果我的数组大小定义成和BufferedInputStream的缓冲区一样大,那我还有必要用缓冲流吗?

带着这些疑问,有必要去读一读BufferedInputStream的源码了,先看一下它的两个成员变量:

private static int defaultBufferSize = 8192;
protected volatile byte buf[];

可以看到,buf就是BufferedInputStream的缓冲区,其实就是一个数组,这个数组有多大呢?那就要找它在哪里赋值的了,如下:

public BufferedInputStream(InputStream in) {
    this(in, defaultBufferSize);
}

public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }

可以看到,是在BufferedInputStream的构造函数中创建的buf缓冲区,大小为defaultBufferSize,也就是8192,也就是8K,所以平时我们在不使用缓冲流时,读取数据的数组定义多大合适呢?就定义成8K就好了,不要去多想为什么是8K,人家写JDK的人就用了这个值,肯定是经过了人家的深思熟虑的,我们只要知道使用8K不会太小,也不会太大,放心用就行了。

知道了缓冲区的大小了,接下来就要看它什么时候往缓冲区里装数据了,肯定是在调用read方法读取数据的时候,我们就看常用的read(byte[])这个方法,这个方法又调用了read(byte b[], int off, int len)方法,而这个方法又调用了read1方法,read1方法如下:

    private int read1(byte[] b, int off, int len) throws IOException {
        int avail = count - pos;
        if (avail <= 0) {
            /* If the requested length is at least as large as the buffer, and
               if there is no mark/reset activity, do not bother to copy the
               bytes into the local buffer.  In this way buffered streams will
               cascade harmlessly. */
            if (len >= getBufIfOpen().length && markpos < 0) {
                return getInIfOpen().read(b, off, len);
            }
            fill();
            avail = count - pos;
            if (avail <= 0) return -1;
        }
        int cnt = (avail < len) ? avail : len;
        System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
        pos += cnt;
        return cnt;
    }

如上源码,有一段英文注释,大家可以拿词典翻译一下什么意思,我简化一下他要表达的意思主要是“如果接收的数组大小长度大于或等于缓冲的大小,则没必要使用缓冲区了”,怎么理解呢?这时需要回顾一下我们读取数据的代码,如下:

byte[] buf = new byte[8192];
int readCount = bufferedInputStream.read(buf);

这下应该理解 了吧,我们使用buf这个数组来接收从缓冲流中读取的数据,而且我们的buf数组大小跟缓冲流里面的缓冲数组大小是一样的,在这种情况下,根据上面的理解其实我们没必要使用缓冲流了。

继续看BufferedInputStream源码,上面的read1方法中调用了一个fill()方法,源码比较多,我们就看关键的一行:

int n = getInIfOpen().read(buffer, pos, buffer.length - pos);

这里的buffer就是缓冲流里面定义的缓冲数组,getInIfOpen()就是拿到缓冲流包装的那个真正的InputStream对象,可以看到它把数据读取到了缓冲数组中,在read1方法中,调用了fill()方法之后还有一句关键代码,如下:

System.arraycopy(getBufIfOpen(), pos, b, off, cnt);

看明显,这是在复制数组,getBufIfOpen()是拿到缓冲数组,而b就是我们传进去的数组对象,这行代码的功能就是从缓冲数组中复制数据到我们的b数组中。

读到这里,缓冲流的原理就差不多理解了,大家如果没有自己去读源码的话,只看我的分析可能有点乱,这里我再整理一下BufferedInputStream的read1源码,大家一看就明白了:

private int read1(byte[] b, int off, int len) throws IOException {
	if (avail <= 0) { // 缓冲区中数据都被读取完了
		
		if (len >= getBufIfOpen().length && markpos < 0) {
			// 如果用户传进来的用于接收数据的数组长度大于或等于缓冲区的长度
			
			// 不需要使用缓冲区,直接从InputStream读取数据到用户的数组中
			return getInIfOpen().read(b, off, len); // 没必要往下走了,直接返回
		}

		 // 如果用户传进来的用于接收数据的数组长度小于缓冲区
		fill(); // 从InputStream读取数所并保存到缓冲区中
	}
	
	// 从缓冲区中复制数据到用户的数组中
	System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
}

OK,这下应该明明白白了,用大白话总结如下(我们把BufferedInpuStream称之为缓冲流):

  • 我们调用缓冲流来读取数据,系统会先看一下缓冲区中有没有可用数据,有的话直接从缓冲区中复制数据给用户
  • 如果缓冲区中没有可用数据,则从真正的InputStream中读一次性读取8K的数据保存在缓冲区中,然后再从缓冲区中复制数据给用户
  • 如果用户接收数据的数组长度大于或等于缓冲区的长度,则系统就不会使用缓存区来保存数据了,而是直接从InputStream中读取数据保存到用户的数组中

使用缓冲流的好处:假设我们有一个文件,大小为8K,我们使用InputStream来读取,每次读取1K,则需要读取8次,也就是要访问文件8次,如果使用缓冲流来读取,你依旧是每次读取1K,也要读取8次,但是在你第一次读取的时候,缓冲流就会从文件中一次读取8K的数据进来,然后复制1K的数据给你,你第二次读取时,再从缓冲区复制1K数据给你,第三次读取时再复制1K给你。。。看到区别了吧,不使用缓冲区,访问了8次文件,使用了缓冲区则只访问了一次文件,当文件很大的时候,访问次数的差别就更大了,效率的差别也会变得很大,所以使用缓冲区可以提升效率,了解了原理后我们知道,这仅限于你读取数据时使用的数组长度小于8K的情况,那似乎了解了原理后,缓冲区没有用了呀,我每次读取时数组长度设置为8K不就完事了吗??

对于BufferedOutputStream原理是一样的,里面有一个8K的缓冲区,如我们有8K的数据,每次写出1K,其实是每次都是把数据写到了缓冲区中,等缓冲中被写满8K后,再调用OutpuStream真正的写出数据到文件,一次写出8K。

我想,缓冲区应该还是有它的作用,只是我们不知道有什么用而已,查看缓冲流的JDK说明,如下:

BufferedInputStream 为另一个输入流添加一些功能,即缓冲输入以及支持 mark 和 reset 方法的能力。在创建 BufferedInputStream 时,会创建一个内部缓冲区数组。在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。mark 操作记录输入流中的某个点,reset 操作使得在从包含的输入流中获取新字节之前,再次读取自最后一次 mark 操作后读取的所有字节。

这里看到了一些别的功能,比如支持mark和reset方法的能力。虽然不知道干嘛的,只能先做个标记,以后再看一些开源大神的源码时,可以看看别人有没有使用缓冲流,以及是怎么使用的。

最后,写个代码验证一下,如果我们定义的数组长度大于等于缓冲流的缓冲区长度时,是否就没必要使用缓冲区了(这里采用了Kotlin语言编写):

private val srcFile = File("E:\\迅雷下载\\ideaIC-2018.3.5.exe")
private val destFile = File("E:\\迅雷下载\\ideaIC-2018.3.5_Copy.exe")

fun copy1() {
    val fis = FileInputStream(srcFile)
    val fos = FileOutputStream(destFile)
    val buf = ByteArray(1024)
    var len: Int
    while (fis.read(buf).also { len = it } != -1) {
        fos.write(buf,0, len)
    }
    fis.close()
    fos.close()
}

fun copy2() {
    val bis = BufferedInputStream(FileInputStream(srcFile))
    val bos = BufferedOutputStream(FileOutputStream(destFile))
    val buf = ByteArray(1024)
    var len: Int
    while (bis.read(buf).also { len = it } != -1) {
        bos.write(buf,0, len)
    }
    bis.close()
    bos.close()
}

这里我使用了一个445M的文件,分别运行copy1和copy2方法,copy1使用了原始的输入输出流,而copy2使用了缓冲流,打印这两个方法的运行时间,如下:

copy1:10138
copy2:8043

接下来,我们把copy1中的数组大小改成8K,再次运行copy1,时间如下:

copy1:7846

看到没,不使用缓冲流,只要把数组长度设置大一些,还更快一点,原因也很简单,不使用缓冲流,就少了数组复制的操作。

接下来,我再把数组长度设置长一些,设置为1M,运行时间如下:

copy1:7172

比设置为8K也没快多少,所以数组长度设置为多少合适,看来8K还是很有讲究的,我们就记住使用8K就行了。

我这个数据也不太准,每次运行时间不太一样,当然,也许我的观点是错误的,也希望当你发现我观点是错误的时候,麻烦给我留言回复一下,为什么要使用缓冲流,而不是直接使用原始流定义数组长度为8K。

你可能感兴趣的:(java,经验知识分享,InputStream,BufferedInput,缓冲区,缓冲,数组大小)