輸入輸出流的緩衝區設置多大比較合適

剛剛在寫代碼,需要把一個文件讀進來,然後壓縮後寫出去,在讀取文件的時候,源代碼如下:

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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章