BufferedInputStream 是一個帶有內存緩衝的 InputStream.
1.首先來看類結構 :
BufferedInputStream是繼承自FilterInputStream。
FilterInputStream繼承自InputStream屬於輸入流中的鏈接流,同時引用了InputStream,將InputStream封裝成一個內部變量,同時構造方法上需要傳入一個InputStream。這是一個典型的裝飾器模式,他的任何子類都可以對一個繼承自InputStream的原始流或其他鏈接流進行裝飾,如我們常用的使用BufferedInputStream對FileInputStream進行裝飾,使普通的文件輸入流具備了內存緩存的功能,通過內存緩衝減少磁盤io次數。
- protected volatile InputStream in;
- protected FilterInputStream(InputStream in) {
- this.in = in;
- }
注意:成員變量in使用了volatile關鍵字修飾,保障了該成員變量多線程情況下的可見性。
2.內存緩衝的實現
概要的瞭解完BufferedInputStream的繼承關係,接下來詳細理解BufferedInputStream是如何實現內存緩衝。既是內存緩衝,就涉及到內存的分配,管理以及如何實現緩衝。
通過構造方法可以看到:初始化了一個byte數組作爲內存緩衝區,大小可以由構造方法中的參數指定,也可以是默認的大小。
- protected volatile byte buf[];
- private static int defaultBufferSize = 8192;
- public BufferedInputStream(InputStream in, int size) {
- super(in);
- if (size <= 0) {
- throw new IllegalArgumentException("Buffer size <= 0");
- }
- buf = new byte[size];
- }
- public BufferedInputStream(InputStream in) {
- this(in, defaultBufferSize);
- }
看完構造函數,大概可以瞭解其實現原理:通過初始化分配一個byte數組,一次性從輸入字節流中讀取多個字節的數據放入byte數組,程序讀取部分字節的時候直接從byte數組中獲取,直到內存中的數據用完再重新從流中讀取新的字節。那麼從api文檔中我們可以瞭解到BufferedStream大概具備如下的功能:
從api可以瞭解到BufferedInputStream除了使用一個byte數組做緩衝外還具備打標記,重置當前位置到標記的位置重新讀取數據,忽略掉n個數據。這些功能都涉及到緩衝內存的管理,首先看下相關的幾個成員變量:
count表示當前緩衝區內總共有多少有效數據;pos表示當前讀取到的位置(即byte數組的當前下標,下次讀取從該位置讀取);markpos:打上標記的位置;marklimit:最多能mark的字節長度,也就是從mark位置到當前pos的最大長度。
從最簡單的read()讀取一個字節的方法開始看:
- public synchronized int read() throws IOException {
- if (pos >= count) {
- fill();
- if (pos >= count)
- return -1;
- }
- return getBufIfOpen()[pos++] & 0xff;
- }
當pos>=count的時候也就是表示當前的byte中的數據爲空或已經被讀完,他調用了一個fill()方法,從字面理解就是填充的意思,實際上是從真正的輸入流中讀取一些新數據放入緩衝內存中,之後直到緩衝內存中的數據讀完前都不會再從真正的流中讀取數據。
看源碼中的fill()方法有很大一段是關於markpos的處理,其處理過程大致如下圖:
a.沒有markpos的情況很簡單:
b.有mark的情況比較複雜:
3.read()方法返回值
以上即爲內存緩衝管理的完全過程,再回過頭看read()方法,當緩衝byte數組中有數據可以讀時,直接從數組中讀取一個字節,但最後的read方法返回的卻是int,而且還和0xff做了與運算。
爲什麼不直接返回一個byte,而是一個與運算後的int。首先宏觀的看InputStream和Reader兩個輸入流的抽象類都定義了read接口而且都返回int,一個是字節流,一個是字符流。我們知道字節用byte表示,字符用char表示。首先看java中基本類型的取值範圍:
從取值範圍來看int包含了char和byte,這爲使用int作爲返回值類型提供了可能。
在應用中我們一般用read()接口的返回值是-1則表示已經讀到文件尾(EOF)。
char的取值範圍本身不包含負數,所有用int的-1表示文件讀完沒問題,但byte的取值範圍-128 ~ 127,包含了-1,讀取的有效數據範圍就是-128~127,沒辦法用這個取值範圍中的任何一個數字表示異常或者數據已經讀完,所以接口如果直接使用byte作爲返回值不可行,直接將byte強制類型轉換成int也不行,因爲如果讀到一個byte的-1,轉爲int了也是-1,會被理解爲文件已經讀完。所以這裏做了一個特殊處理return getBufIfOpen()[pos++] & 0xff。
0xff是int類型,二進制爲0000 0000 0000 0000 0000 0000 1111 1111。
上述的與運算實際上讀取的byte先被強制轉換成了int,例如byte的-1(最高位表示符號位,以補碼的形式表示負數爲:1111 1111)
轉換爲int之後的二進制1111 1111 1111 1111 1111 1111 1111 1111
& 0xff之後高位去0
最後返回的結果是0000 0000 0000 0000 0000 0000 1111 1111, 爲int值爲256
其-128~-1被轉爲int中128~256的正數表示。
這樣解決了可以用-1表示文件已經讀完。但關鍵是數據的值發生了變化,真正要用讀取的數據時是否還能拿到原始的byte。還拿上面那個例子來看,當讀取返回一個256時,將其強制類型轉換爲byte,(byte)256得到byte的-1,因爲byte只有8位,當int的高位被丟棄後就只剩下1111 1111,在byte中高位的1表示符號位爲負數,最終的結果即是byte的-1;同樣byte的-128(1000 0000)被轉爲int的128(0000 0000 0000 0000 0000 0000 1000 0000),強制類型轉換後還原byte的1000 0000。
4.線程安全
返回值中還有一個細節是getBufIfOpen()[pos++],直接將pos++來獲取下一個未讀取的數據,這裏涉及到的兩個元素:一個內存數組,一個當前讀取的數據下標都是全局變量,pos++也不是線程安全。那麼BufferedInputStream如何保證對內存緩衝數組的操作線程安全?源碼中有操作的public方法除了close方法之外,其他方法上都加上了synchronized關鍵字,以保障上面描述的整個內存緩存數組的操作是線程安全的。但爲什麼close方法沒有synchronized,我們看這個方法做了些什麼事情:
- byte[] buffer;
- while ( (buffer = buf) != null) {
- if (bufUpdater.compareAndSet(this, buffer, null)) {
- InputStream input = in;
- in = null;
- if (input != null)
- input.close();
- return;
- }
- // Else retry in case a new buf was CASed in fill()
- }
簡單來看做了兩個操作:把內存數組置爲null,將引用的inputStream置爲null,同時將引用的inputStream.close();
這兩個操作的核心都是關閉原始流,釋放資源,如果加了synchronized關鍵字,會導致當前線程正在執行read方法,而且系統消耗很大時,想釋放資源無法釋放。此時read方法還沒執行完,我們知道synchronized的鎖是加在整個對象上的,所以close方法就必須等到read結束後才能執行,這樣很明顯不能滿足close的需求,甚至會導致大量的io資源被阻塞不能關閉。
但該方法用一個while循環,而且只有當bufUpdater.compareAndSet(this, buffer, null)成功時,才執行上述的資源釋放。
先看bufUpdater這個全局變量:
- protected volatile byte buf[];
- private static final
- AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
- AtomicReferenceFieldUpdater.newUpdater
- (BufferedInputStream.class, byte[].class, "buf");
AtomicReferenceFieldUpdater是一個抽象類,但該類的內部已經給出了包訪問控制級別的一個實現AtomicReferenceFieldUpdaterImpl,原理是利用反射將一個 被聲明成volatile 的屬性通過JNI調用,使用cpu指令級的命令將一個變量進行更新,保障該操作是原子的。也就是通過上面定義的bufUpdater將buf這個byte數組的跟新變爲原子操作,其作用是保障其原子更新。
BufferedInputStream源代碼中總共有兩個地方用到了這個bufUpdater,一個是我們上面看到的close方法中,另外一個是再前面說道的fill()方法中。既然BufferedInputStream的所有操作上都用了synchronized來做同步,那爲什麼這裏還需要用這個原子更新器呢?帶着問題上面提到過fill()方法中的最後一個步驟:當有mark,而且markLimit的長度又大於初始數組的長度時,需要對內存數組擴容,即創建一個尺寸更大的數組,將原來數組中的數據拷貝到新數組中,再將指向原數組的應用指向新的數組。bufUpdater正是用在了將原數組引用指向新數組的操作上,同樣close的方法使用的bufUpdater也是用在對數組引用的改變上,這樣看來就比較清晰了,主要是爲了防止一個線程在執行close方法時,將buffer賦值爲null這個時候另外一個線程正在執行fill()方法的最後一個步驟又將buffer賦值給了一個新的數組,從而導致資源沒有釋放掉。
5.結束
到這裏BufferedInputStream的源碼每個細節都已經分析完,看似簡單的一些方法,返回值和調用中其實蘊藏着很多不簡單的東西,通過閱讀一些好的源代碼可以學到不少東西。