天天看點

BufferedInputStream 源碼學習筆記

bufferedinputstream是一個帶有記憶體緩沖的inputstream.

bufferedinputstream是繼承自filterinputstream。

filterinputstream繼承自inputstream屬于輸入流中的連結流,同時引用了inputstream,将inputstream封裝成一個内部變量,同時構造方法上需要傳入一個inputstream。這是一個典型的裝飾器模式,他的任何子類都可以對一個繼承自inputstream的原始流或其他連結流進行裝飾,如我們常用的使用bufferedinputstream對fileinputstream進行裝飾,使普通的檔案輸入流具備了記憶體緩存的功能,通過記憶體緩沖減少磁盤io次數。

java代碼

注意:成員變量in使用了volatile關鍵字修飾,保障了該成員變量多線程情況下的可見性。

2.記憶體緩沖的實作

概要的了解完bufferedinputstream的繼承關系,接下來詳細了解bufferedinputstream是如何實作記憶體緩沖。既是記憶體緩沖,就涉及到記憶體的配置設定,管理以及如何實作緩沖。

通過構造方法可以看到:初始化了一個byte數組作為記憶體緩沖區,大小可以由構造方法中的參數指定,也可以是預設的大小。

看完構造函數,大概可以了解其實作原理:通過初始化配置設定一個byte數組,一次性從輸入位元組流中讀取多個位元組的資料放入byte數組,程式讀取部分位元組的時候直接從byte數組中擷取,直到記憶體中的資料用完再重新從流中讀取新的位元組。那麼從api文檔中我們可以了解到bufferedstream大概具備如下的功能

從api可以了解到bufferedinputstream除了使用一個byte數組做緩沖外還具備打标記,重置目前位置到标記的位置重新讀取資料,忽略掉n個資料。這些功能都涉及到緩沖記憶體的管理,首先看下相關的幾個成員變量:

count表示目前緩沖區内總共有多少有效資料;pos表示目前讀取到的位置(即byte數組的目前下标,下次讀取從該位置讀取);markpos:打上标記的位置;marklimit:最多能mark的位元組長度,也就是從mark位置到目前pos的最大長度。

從最簡單的read()讀取一個位元組的方法開始看:

當pos>=count的時候也就是表示目前的byte中的資料為空或已經被讀完,他調用了一個fill()方法,從字面了解就是填充的意思,實際上是從真正的輸入流中讀取一些新資料放入緩沖記憶體中,之後直到緩沖記憶體中的資料讀完前都不會再從真正的流中讀取資料。

看源碼中的fill()方法有很大一段是關于markpos的處理,其處理過程大緻為

a.沒有markpos的情況很簡單

b.有mark的情況比較複雜:

c.read()方法傳回值

以上即為記憶體緩沖管理的完全過程,再回過頭看read()方法,當緩沖byte數組中有資料可以讀時,直接從數組中讀取一個位元組,但最後的read方法傳回的卻是int,而且還和0xff做了與運算。

return getbufifopen()[pos++] & 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,我們看這個方法做了些什麼事情:

簡單來看做了兩個操作:把記憶體數組置為null,将引用的inputstream置為null,同時将引用的inputstream.close();

這兩個操作的核心都是關閉原始流,釋放資源,如果加了synchronized關鍵字,會導緻目前線程正在執行read方法,而且系統消耗很大時,想釋放資源無法釋放。此時read方法還沒執行完,我們知道synchronized的鎖是加在整個對象上的,是以close方法就必須等到read結束後才能執行,這樣很明顯不能滿足close的需求,甚至會導緻大量的io資源被阻塞不能關閉。

但該方法用一個while循環,而且隻有當bufupdater.compareandset(this, buffer, null)成功時,才執行上述的資源釋放。

先看bufupdater這個全局變量:

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的源碼每個細節都已經分析完,看似簡單的一些方法,傳回值和調用中其實蘊藏着很多不簡單的東西,通過閱讀一些好的源代碼可以學到不少東西。