1. 程式人生 > >Java IO流分析、IO整理與IO優化

Java IO流分析、IO整理與IO優化



一、IO流的概念

Java中對檔案的操作是以流的方式進行的。流是Java記憶體中的一組有序資料序列。Java將資料從源(檔案、記憶體、鍵盤、網路)讀入到記憶體中,形成了流,然後將這些流還可以寫到另外的目的地(檔案、記憶體、控制檯、網路),之所以稱為流,是因為這個資料序列在不同時刻所操作的是源的不同部分。

二、IO流的分類

Java中的流,可以從不同的角度進行分類。

按照資料流的方向不同可以分為:輸入流和輸出流。

按照處理資料單位不同可以分為:位元組流和字元流。

按照實現功能不同可以分為:節點流和處理流。

位元組流:一次讀入或讀出是8位二進位制。

字元流:一次讀入或讀出是16位二進位制。

位元組流和字元流的原理是相同的,只不過處理的單位不同而已。字尾是Stream是位元組流,而後綴是Reader,Writer是字元流。

節點流:直接與資料來源相連,讀入或讀出。

直接使用節點流,讀寫不方便,為了更快的讀寫檔案,才有了處理流。

處理流:與節點流一塊使用,在節點流的基礎上,再套接一層,套接在節點流上的就是處理流。

Jdk提供的流繼承了四大類:InputStream(位元組輸入流),OutputStream(位元組輸出流),Reader(字元輸入流),Writer(字元輸出流)。

2.1 關於位元組流和字元流的區別

實際上位元組流在操作的時候本身是不會用到緩衝區的,是檔案本身的直接操作的,但是字元流在操作的 時候下後是會用到緩衝區的,是通過緩衝區來操作檔案的。

讀者可以試著將上面的位元組流和字元流的程式的最後一行關閉檔案的程式碼註釋掉,然後執行程式看看。你就會發現使用位元組流的話,檔案中已經存在內容,但是使用字元流的時候,檔案中還是沒有內容的,這個時候就要重新整理緩衝區。

使用位元組流好還是字元流好呢?

答案是位元組流。首先因為硬碟上的所有檔案都是以位元組的形式進行傳輸或者儲存的,包括圖片等內容。但是字元只是在記憶體中才會形成的,所以在開發中,位元組流使用廣泛。

三、IO框架結構

我們以位元組流與字元流的分類來分析io類的關係與結構。

3.1 基於位元組的 IO 操作介面

基於位元組的 IO 操作介面輸入和輸出分別是:InputStream 和 OutputStream,InputStream 輸入流的類繼承層次如下圖所示:

圖 1. InputStream 相關類層次結構


輸入流根據資料型別和操作方式又被劃分成若干個子類,每個子類分別處理不同操作型別,OutputStream 輸出流的類層次結構也是類似,如下圖所示:

圖 2. OutputStream 相關類層次結構


這裡就不詳細解釋每個子類如何使用了,如果不清楚的話可以參考一下 JDK 的 API 說明文件,這裡只想說明兩點,一個是操作資料的方式是可以組合使用的,如這樣組合使用

[java] view plaincopyprint?
  1. OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream("fileName"));  
OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream("fileName"));

還有一點是流最終寫到什麼地方必須要指定,要麼是寫到磁碟要麼是寫到網路中,其實從上面的類圖中我們發現,寫網路實際上也是寫檔案,只不過寫網路還有一步需要處理就是底層作業系統再將資料傳送到其它地方而不是本地磁碟。關於網路 I/O 和磁碟 I/O 我們將在後面詳細介紹。

3.2 基於字元的 I/O 操作介面

不管是磁碟還是網路傳輸,最小的儲存單元都是位元組,而不是字元,所以 I/O 操作的都是位元組而不是字元,但是為啥有操作字元的 I/O 介面呢?這是因為我們的程式中通常操作的資料都是以字元形式,為了操作方便當然要提供一個直接寫字元的 I/O 介面,如此而已。我們知道字元到位元組必須要經過編碼轉換,而這個編碼又非常耗時,而且還會經常出現亂碼問題,所以 I/O 的編碼問題經常是讓人頭疼的問題。關於 I/O 編碼問題請參考另一篇文章 《深入分析Java中的中文編碼問題》

下圖是寫字元的 I/O 操作介面涉及到的類,Writer 類提供了一個抽象方法 write(char cbuf[], int off, int len) 由子類去實現。

圖 3. Writer 相關類層次結構

 

讀字元的操作介面也有類似的類結構,如下圖所示:

圖 4.Reader 類層次結構


讀字元的操作介面中也是 int read(char cbuf[], int off, int len),返回讀到的 n 個位元組數,不管是 Writer 還是 Reader 類它們都只定義了讀取或寫入的資料字元的方式,也就是怎麼寫或讀,但是並沒有規定資料要寫到哪去,寫到哪去就是我們後面要討論的基於磁碟和網路的工作機制。

3.3 位元組與字元的轉化介面

另外資料持久化或網路傳輸都是以位元組進行的,所以必須要有字元到位元組或位元組到字元的轉化。字元到位元組需要轉化,其中讀的轉化過程如下圖所示:

圖 5. 字元解碼相關類結構


InputStreamReader 類是位元組到字元的轉化橋樑,InputStream 到 Reader 的過程要指定編碼字符集,否則將採用作業系統預設字符集,很可能會出現亂碼問題。StreamDecoder 正是完成位元組到字元的解碼的實現類。也就是當你用如下方式讀取一個檔案時:

清單 1.讀取檔案

[java] view plaincopyprint?
  1. try {   
  2.             StringBuffer str = new StringBuffer();   
  3.             char[] buf = newchar[1024];   
  4.             FileReader f = new FileReader("file");   
  5.             while(f.read(buf)>0){   
  6.                 str.append(buf);   
  7.             }   
  8.             str.toString();   
  9.  } catch (IOException e) {}  
try { 
            StringBuffer str = new StringBuffer(); 
            char[] buf = new char[1024]; 
            FileReader f = new FileReader("file"); 
            while(f.read(buf)>0){ 
                str.append(buf); 
            } 
            str.toString(); 
 } catch (IOException e) {}


FileReader 類就是按照上面的工作方式讀取檔案的,FileReader 是繼承了 InputStreamReader 類,實際上是讀取檔案流,然後通過 StreamDecoder 解碼成 char,只不過這裡的解碼字符集是預設字符集。

寫入也是類似的過程如下圖所示:

圖 6. 字元編碼相關類結構


通過 OutputStreamWriter 類完成,字元到位元組的編碼過程,由 StreamEncoder 完成編碼過程。

四、具體的IO實現類

檔案進行操作:FileInputStream(位元組輸入流),FileOutputStream(位元組輸出流),FileReader(字元輸入流),FileWriter(字元輸出流)

管道進行操作:PipedInputStream(位元組輸入流),PipedOutStream(位元組輸出流),PipedReader(字元輸入流),PipedWriter(字元輸出流)

PipedInputStream的一個例項要和PipedOutputStream的一個例項共同使用,共同完成管道的讀取寫入操作。主要用於執行緒操作。

位元組/字元陣列:ByteArrayInputStream,ByteArrayOutputStream,CharArrayReader,CharArrayWriter是在記憶體中開闢了一個位元組或字元陣列。

Buffered緩衝流:BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter,是帶緩衝區的處理流,緩衝區的作用的主要目的是:避免每次和硬碟打交道,提高資料訪問的效率。

轉化流:InputStreamReader/OutputStreamWriter,把位元組轉化成字元。

資料流:DataInputStream,DataOutputStream。

因為平時若是我們輸出一個8個位元組的long型別或4個位元組的float型別,那怎麼辦呢?可以一個位元組一個位元組輸出,也可以把轉換成字串輸出,但是這樣轉換費時間,若是直接輸出該多好啊,因此這個資料流就解決了我們輸出資料型別的困難。資料流可以直接輸出float型別或long型別,提高了資料讀寫的效率。

列印流:printStream,printWriter,一般是列印到控制檯,可以進行控制列印的地方。

物件流:ObjectInputStream,ObjectOutputStream,把封裝的物件直接輸出,而不是一個個在轉換成字串再輸出。

序列化流:SequenceInputStream。

物件序列化:把物件直接轉換成二進位制,寫入介質中。

使用物件流需要實現Serializable介面,否則會報錯。而若用transient關鍵字修飾成員變數,不寫入該成員變數,若是引用型別的成員變數為null,值型別的成員變數為0.

五、IO 調優

下面就磁碟 IO 和網路 IO 的一些常用的優化技巧進行總結如下:

5.1 磁碟 I/O 優化

5.1.1 效能檢測

我們的應用程式通常都需要訪問磁碟讀取資料,而磁碟 I/O 通常都很耗時,我們要判斷 I/O 是否是一個瓶頸,我們有一些引數指標可以參考:

如我們可以壓力測試應用程式看系統的 I/O wait 指標是否正常,例如測試機器有 4 個 CPU,那麼理想的 I/O wait 引數不應該超過 25%,如果超過 25% 的話,I/O 很可能成為應用程式的效能瓶頸。Linux 作業系統下可以通過 iostat 命令檢視。

通常我們在判斷 I/O 效能時還會看另外一個引數就是 IOPS,我們應用程式需要最低的 IOPS 是多少,而我們的磁碟的 IOPS 能不能達到我們的要求。每個磁碟的 IOPS 通常是在一個範圍內,這和儲存在磁碟的資料塊的大小和訪問方式也有關。但是主要是由磁碟的轉速決定的,磁碟的轉速越高磁碟的 IOPS 也越高。

現在為了提高磁碟 I/O 的效能,通常採用一種叫 RAID 的技術,就是將不同的磁碟組合起來來提高 I/O 效能,目前有多種 RAID 技術,每種 RAID 技術對 I/O 效能提升會有不同,可以用一個 RAID 因子來代表,磁碟的讀寫吞吐量可以通過 iostat 命令來獲取,於是我們可以計算出一個理論的 IOPS 值,計算公式如下所以:

( 磁碟數 * 每塊磁碟的 IOPS)/( 磁碟讀的吞吐量 +RAID 因子 * 磁碟寫的吞吐量 )=IOPS

5.1.2 提升 I/O 效能

提升磁碟 I/O 效能通常的方法有:

  1. 增加快取,減少磁碟訪問次數
  2. 優化磁碟的管理系統,設計最優的磁碟訪問策略,以及磁碟的定址策略,這裡是在底層作業系統層面考慮的。
  3. 設計合理的磁碟儲存資料塊,以及訪問這些資料塊的策略,這裡是在應用層面考慮的。如我們可以給存放的資料設計索引,通過定址索引來加快和減少磁碟的訪問,還有可以採用非同步和非阻塞的方式加快磁碟的訪問效率。
  4. 應用合理的 RAID 策略提升磁碟 IO,每種 RAID 的區別我們可以用下表所示:

表 1.RAID 策略
磁碟陣列 說明
RAID 0 資料被平均寫到多個磁碟陣列中,寫資料和讀資料都是並行的,所以磁碟的 IOPS 可以提高一倍。
RAID 1 RAID 1 的主要作用是能夠提高資料的安全性,它將一份資料分別複製到多個磁碟陣列中。並不能提升 IOPS 但是相同的資料有多個備份。通常用於對資料安全性較高的場合中。
RAID 5 這中設計方式是前兩種的折中方式,它將資料平均寫到所有磁碟陣列總數減一的磁碟中,往另外一個磁碟中寫入這份資料的奇偶校驗資訊。如果其中一個磁碟損壞,可以通過其它磁碟的資料和這個資料的奇偶校驗資訊來恢復這份資料。
RAID 0+1 如名字一樣,就是根據資料的備份情況進行分組,一份資料同時寫到多個備份磁碟分組中,同時多個分組也會並行讀寫。

5.2、網路 I/O 優化

網路 I/O 優化通常有一些基本處理原則:

  1. 一個是減少網路互動的次數:要減少網路互動的次數通常我們在需要網路互動的兩端會設定快取,比如 Oracle 的 JDBC 驅動程式,就提供了對查詢的 SQL 結果的快取,在客戶端和資料庫端都有,可以有效的減少對資料庫的訪問。關於 Oracle JDBC 的記憶體管理可以參考《 Oracle JDBC 記憶體管理》。除了設定快取還有一個辦法是,合併訪問請求:如在查詢資料庫時,我們要查 10 個 id,我可以每次查一個 id,也可以一次查 10 個 id。再比如在訪問一個頁面時通過會有多個 js 或 css 的檔案,我們可以將多個 js 檔案合併在一個 HTTP 連結中,每個檔案用逗號隔開,然後傳送到後端 Web 伺服器根據這個 URL 連結,再拆分出各個檔案,然後打包再一併發回給前端瀏覽器。這些都是常用的減少網路 I/O 的辦法。
  2. 減少網路傳輸資料量的大小:減少網路資料量的辦法通常是將資料壓縮後再傳輸,如 HTTP 請求中,通常 Web 伺服器將請求的 Web 頁面 gzip 壓縮後在傳輸給瀏覽器。還有就是通過設計簡單的協議,儘量通過讀取協議頭來獲取有用的價值資訊。比如在代理程式設計時,有 4 層代理和 7 層代理都是來儘量避免要讀取整個通訊資料來取得需要的資訊。
  3. 儘量減少編碼:通常在網路 I/O 中資料傳輸都是以位元組形式的,也就是通常要序列化。但是我們傳送要傳輸的資料都是字元形式的,從字元到位元組必須編碼。但是這個編碼過程是比較耗時的,所以在要經過網路 I/O 傳輸時,儘量直接以位元組形式傳送。也就是儘量提前將字元轉化為位元組,或者減少字元到位元組的轉化過程。
  4. 根據應用場景設計合適的互動方式:所謂的互動場景主要包括同步與非同步阻塞與非阻塞方式,下面將詳細介紹。

5.2.1 同步與非同步

所謂同步就是一個任務的完成需要依賴另外一個任務時,只有等待被依賴的任務完成後,依賴的任務才能算完成,這是一種可靠的任務序列。要麼成功都成功,失敗都失敗,兩個任務的狀態可以保持一致。而非同步是不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什麼工作,依賴的任務也立即執行,只要自己完成了整個任務就算完成了。至於被依賴的任務最終是否真正完成,依賴它的任務無法確定,所以它是不可靠的任務序列。我們可以用打電話和發簡訊來很好的比喻同步與非同步操作。

在設計到 IO 處理時通常都會遇到一個是同步還是非同步的處理方式的選擇問題。因為同步與非同步的 I/O 處理方式對呼叫者的影響很大,在資料庫產品中都會遇到這個問題。因為 I/O 操作通常是一個非常耗時的操作,在一個任務序列中 I/O 通常都是效能瓶頸。但是同步與非同步的處理方式對程式的可靠性影響非常大,同步能夠保證程式的可靠性,而非同步可以提升程式的效能,必須在可靠性和效能之間做個平衡,沒有完美的解決辦法。

5.2.2 阻塞與非阻塞

阻塞與非阻塞主要是從 CPU 的消耗上來說的,阻塞就是 CPU 停下來等待一個慢的操作完成 CPU 才接著完成其它的事。非阻塞就是在這個慢的操作在執行時 CPU 去幹其它別的事,等這個慢的操作完成時,CPU 再接著完成後續的操作。雖然表面上看非阻塞的方式可以明顯的提高 CPU 的利用率,但是也帶了另外一種後果就是系統的執行緒切換增加。增加的 CPU 使用時間能不能補償系統的切換成本需要好好評估。

5.2.3 兩種的方式的組合

組合的方式可以由四種,分別是:同步阻塞、同步非阻塞、非同步阻塞、非同步非阻塞,這四種方式都對 I/O 效能有影響。下面給出分析,並有一些常用的設計用例參考。

表 2. 四種組合方式

組合方式 效能分析
同步阻塞 最常用的一種用法,使用也是最簡單的,但是 I/O 效能一般很差,CPU 大部分在空閒狀態。
同步非阻塞 提升 I/O 效能的常用手段,就是將 I/O 的阻塞改成非阻塞方式,尤其在網路 I/O 是長連線,同時傳輸資料也不是很多的情況下,提升效能非常有效。
這種方式通常能提升 I/O 效能,但是會增加 CPU 消耗,要考慮增加的 I/O 效能能不能補償 CPU 的消耗,也就是系統的瓶頸是在 I/O 還是在 CPU 上。
非同步阻塞 這種方式在分散式資料庫中經常用到,例如在網一個分散式資料庫中寫一條記錄,通常會有一份是同步阻塞的記錄,而還有兩至三份是備份記錄會寫到其它機器上,這些備份記錄通常都是採用非同步阻塞的方式寫 I/O。
非同步阻塞對網路 I/O 能夠提升效率,尤其像上面這種同時寫多份相同資料的情況。
非同步非阻塞 這種組合方式用起來比較複雜,只有在一些非常複雜的分散式情況下使用,像叢集之間的訊息同步機制一般用這種 I/O 組合方式。如 Cassandra 的 Gossip 通訊機制就是採用非同步非阻塞的方式。
它適合同時要傳多份相同的資料到叢集中不同的機器,同時資料的傳輸量雖然不大,但是卻非常頻繁。這種網路 I/O 用這個方式效能能達到最高。

雖然非同步和非阻塞能夠提升 I/O 的效能,但是也會帶來一些額外的效能成本,例如會增加執行緒數量從而增加 CPU 的消耗,同時也會導致程式設計的複雜度上升。如果設計的不合理的話反而會導致效能下降。在實際設計時要根據應用場景綜合評估一下。

下面舉一些非同步和阻塞的操作例項:

在 Cassandra 中要查詢資料通常會往多個數據節點發送查詢命令,但是要檢查每個節點返回資料的完整性,所以需要一個非同步查詢同步結果的應用場景,部分程式碼如下:


清單 3.非同步查詢同步結果
[java] view plaincopyprint?
  1. class AsyncResult implements IAsyncResult{   
  2.     privatebyte[] result_;   
  3.     private AtomicBoolean done_ = new AtomicBoolean(false);   
  4.     private Lock lock_ = new ReentrantLock();   
  5.     private Condition condition_;   
  6.     privatelong startTime_;   
  7.     public AsyncResult(){          
  8.         condition_ = lock_.newCondition();// 建立一個鎖
  9.         startTime_ = System.currentTimeMillis();   
  10.     }      
  11.  /*** 檢查需要的資料是否已經返回,如果沒有返回阻塞 */
  12.  publicbyte[] get(){   
  13.         lock_.lock();   
  14.         try{   
  15.             if (!done_.get()){condition_.await();}   
  16.         }catch (InterruptedException ex){   
  17.             thrownew AssertionError(ex);   
  18.         }finally{lock_.unlock();}   
  19.         return result_;   
  20.  }   
  21.  /*** 檢查需要的資料是否已經返回 */
  22.     publicboolean isDone(){return done_.get();}   
  23.  /*** 檢查在指定的時間內需要的資料是否已經返回,如果沒有返回丟擲超時異常 */
  24.     publicbyte[] get(long timeout, TimeUnit tu) throws TimeoutException{   
  25.         lock_.lock();   
  26.         try{            boolean bVal = true;   
  27.             try{   
  28.                 if ( !done_.get() ){   
  29.            long overall_timeout = timeout - (System.currentTimeMillis() - startTime_);   
  30.                     if(overall_timeout > 0)// 設定等待超時的時間
  31.                         bVal = condition_.await(overall_timeout, TimeUnit.MILLISECONDS);   
  32.                     else bVal = false;   
  33.                 }   
  34.             }catch (InterruptedException ex){   
  35.                 thrownew AssertionError(ex);   
  36.             }   
  37.             if ( !bVal && !done_.get() ){// 丟擲超時異常
  38.                 thrownew TimeoutException("Operation timed out.");   
  39.             }   
  40.         }finally{lock_.unlock();      }   
  41.         return result_;   
  42.  }   
  43.  /*** 該函式拱另外一個執行緒設定要返回的資料,並喚醒在阻塞的執行緒 */
  44.     publicvoid result(Message response){          
  45.         try{   
  46.             lock_.lock();   
  47.             if ( !done_.get() ){                  
  48.                 result_ = response.getMessageBody();// 設定返回的資料
  49.                 done_.set(true);   
  50.                 condition_.signal();// 喚醒阻塞的執行緒
  51.             }   
  52.         }finally{lock_.unlock();}          
  53.     }      
  54.  }   
class AsyncResult implements IAsyncResult{ 
    private byte[] result_; 
    private AtomicBoolean done_ = new AtomicBoolean(false); 
    private Lock lock_ = new ReentrantLock(); 
    private Condition condition_; 
    private long startTime_; 
    public AsyncResult(){        
        condition_ = lock_.newCondition();// 建立一個鎖
        startTime_ = System.currentTimeMillis(); 
    }    
 /*** 檢查需要的資料是否已經返回,如果沒有返回阻塞 */ 
 public byte[] get(){ 
        lock_.lock(); 
        try{ 
            if (!done_.get()){condition_.await();} 
        }catch (InterruptedException ex){ 
            throw new AssertionError(ex); 
        }finally{lock_.unlock();} 
        return result_; 
 } 
 /*** 檢查需要的資料是否已經返回 */ 
    public boolean isDone(){return done_.get();} 
 /*** 檢查在指定的時間內需要的資料是否已經返回,如果沒有返回丟擲超時異常 */ 
    public byte[] get(long timeout, TimeUnit tu) throws TimeoutException{ 
        lock_.lock(); 
        try{            boolean bVal = true; 
            try{ 
                if ( !done_.get() ){ 
           long overall_timeout = timeout - (System.currentTimeMillis() - startTime_); 
                    if(overall_timeout > 0)// 設定等待超時的時間
                        bVal = condition_.await(overall_timeout, TimeUnit.MILLISECONDS); 
                    else bVal = false; 
                } 
            }catch (InterruptedException ex){ 
                throw new AssertionError(ex); 
            } 
            if ( !bVal && !done_.get() ){// 丟擲超時異常
                throw new TimeoutException("Operation timed out."); 
            } 
        }finally{lock_.unlock();      } 
        return result_; 
 } 
 /*** 該函式拱另外一個執行緒設定要返回的資料,並喚醒在阻塞的執行緒 */ 
    public void result(Message response){        
        try{ 
            lock_.lock(); 
            if ( !done_.get() ){                
                result_ = response.getMessageBody();// 設定返回的資料
                done_.set(true); 
                condition_.signal();// 喚醒阻塞的執行緒
            } 
        }finally{lock_.unlock();}        
    }    
 } 

參考:
Java IO流分析整理
http://blog.csdn.net/llhhyy1989/article/details/7388059
JDK API , java io 札記整理-從模式看JDK IO
http://www.myexception.cn/program/842855.html
深入分析 Java I/O 的工作機制
https://www.ibm.com/developerworks/cn/java/j-lo-javaio/
深入分析 Java 中的中文編碼問題
http://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/
java中的IO整理
http://blog.csdn.net/a511596982/article/details/8284358
Java IO學習筆記:概念原理
http://lavasoft.blog.51cto.com/62575/95384