Java IO與NIO的相關問題 – JAVA編程語言程序開發技術文章

流(Stream)是最早的Java對IO的抽象,而通道(Channel)是NIO對新Java對IO的抽象,通道與流的不同之處在於通道是雙向的。而流隻是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類), 而 通道 可以用於讀、寫或者同時用於讀寫。流和通道的基本單位都是字節,但是流是以字節數組作為緩沖區中介,而通道是以ByteBuffer來作為緩沖區中介。
     流中包含的字節如流水一樣,一旦流過去,就無法再使用。但由於流的實現是抽象類,在其子類中可以選擇覆寫父類的某些操作,所以子類輸入輸出流可能會有額外的控制操作,以便實現流的部分內容的重新讀取,如流的標記(mark)和重置功能(reset),但並不是每一種流都有這兩種功能,需要有相應的實現,可通過markSupported方法來判斷是否支持標記功能。因為流無法復用,假如有一個流有多個接收者,那怎麼辦?上面介紹的標記和重置功能是其中一個方法,也可以把流中的全部數據保存到一個字節數組中,在不同的接受著中數據傳遞是通過這個字節數組來完成,而不是使用流的對象。其中流的標記和重置功能也是通過第二種方法實現的,隻是這個字節數組的保存和操作是又流的某些子類來實現,如BufferedInputStream類。
     調用流的read方法時,如果沒有足夠的數據可以用,則read方法會被阻塞,直到當前的流成功的完成準備為止。從流中讀取的數據並不是馬上進入目的介質,而是先放入字節數組等緩沖區,等合適的時機再執行實際的寫入操作。當然可以通過調用flush方法強制寫入,註意在緩沖區滿或者流關閉的時候,也會自動執行實際的寫入操作。
  字節流是處理字節的,Java IO還有一種處理字符的字符流,即java.io.Reader類和java.io.Writer類及其子類,字符類一般是通過字節流InputStream類和OutputStream類來創建,對應的是InputStreamReader類和OutputStreamWriter類,隻要指定編碼格式就行瞭。
      
   Java NIO的通道使用的基本緩沖區是ByteBuffer,其中有3個基本狀態變量:容量(capacity),就是緩沖區的大小;讀寫限制(limit),總大小中允許讀寫的最大位置;讀寫位置(position),當前讀寫的位置。這三個狀態變量都是以字節為單位的,假如是CharBuffer,則是以字符為單位,IntBuffer則是以整數為單位,等等。
ByteBuffer的基本方法: clear,把讀寫限制設為緩沖區的容量,同時把讀寫位置設為0,flip方法,把讀寫限制設為當前的讀寫位置,同時把讀寫位置設為0,rewind方法,不改變讀寫限制,僅把讀寫位置設為0,compact方法,把當前讀寫位置到讀寫限制范圍內的數據復制到內部存儲空間的最前面,然後再把讀寫位置移動到緊接著復制完成的數據的下一個位置,讀寫限制設置為容量的大小。ByteBuffer的實現可分為直接緩沖區和非直接緩沖區,直接緩沖區直接使用操作系統底層的IO操作來完成,提升瞭讀寫操作時的性能,不過也帶來瞭額外的創建和銷毀時的代價,直接緩沖區一般是常駐內存,會增加內存開銷,一般用在對性能較高的情況。以下通過三個簡單的例子來理解流和通道對文件復制的操作。
[java]
      //使用流來實現文件的復制 
public static void copyFileByStream(String src,String dest) throws IOException{ 
 FileInputStream in=new FileInputStream(src); 
       File file=new File(dest); 
       if(!file.exists()) 
           file.createNewFile(); 
       FileOutputStream out=new FileOutputStream(file); 
       int c; 
       byte buffer[]=new byte[1024];  //每次讀取的字節數 
       while((c=in.read(buffer))!=-1){ 
               out.write(buffer);         
       } 
       in.close(); 
       out.close(); 
   } 
//使用ByteBuffer作為緩沖區來實現文件的復制 
public static void copyFileByByteBuffer(String src,String dest)  throws IOException{ 
       ByteBuffer buffer = ByteBuffer.allocateDirect(32*1024);//分配ByteBuffer的容量大小 
       FileInputStream in=new FileInputStream(src); 
       FileOutputStream out=new FileOutputStream(dest); 
    FileChannel s = in.getChannel(); 
    FileChannel d = out.getChannel();   
    while(s.read(buffer)>0||buffer.position()!=0){ //通過在通道上使用ByteBuffer來傳輸,不需要記錄每次實際讀取的字節數 
            buffer.flip(); 
            d.write(buffer); 
            buffer.compact(); 
        } 
 
   } 
 
//使用通道的傳輸方法來實現文件的復制 
public static void copyFileByChannelTransfer(String src,String dest)  throws IOException{ 
       ByteBuffer buffer = ByteBuffer.allocateDirect(32*1024); 
       FileInputStream in=new FileInputStream(src); 
       FileOutputStream out=new FileOutputStream(dest); 
    FileChannel s = in.getChannel(); 
    FileChannel d = out.getChannel(); 
    s.transferTo(0, s.size(), d);//直接從一個通道傳輸到另一個通道。 
     
   } 
由以上可以看出,使用ByteBuffer類不需要像流一樣記錄每次實際讀取的字節數,隻要分配一個固定大小的ByteBuffer緩沖區就行瞭,文件通道的transferTo方法使得數據傳輸更加簡單。
   對大文件的操作一般使用ByteBuffer的子類MappedByteBuffer,該類將系統的內存地址映射到要操作的文件上,操作這些內存地址就相當於讀取文件的內容,這樣就大大提高瞭操作文件的性能。具體用法看相關文檔。
   FileChannel類還有lock和tryLock方法可對文件進行加鎖,但是該加鎖是在虛擬機級別的,對於虛擬機上的多線程程序,不能用這種加鎖機制來協同不同的線程對文件的訪問。FileChannel類的加鎖是應用程序與應用程序之間的加鎖。
   由於傳統的Socket類和ServerSocket類中提供的與建立連接和數據傳輸相關的方法都是阻塞式的,套接字通道提供瞭非阻塞式的和多路復用的套接字連接。多路復用套接字需要通過一個選擇器(Selector)來對所有的套接字通道進行管理監聽,當其中的某些套接字通道上有Selector感興趣的事件發生的時候,這個通道就會變成可用的狀態,然後可以進行各種通道操作。不過需要在一開始的時候把我們需要監聽的套接字通道註冊到選擇器(Selector)上,以表明我們需要由選擇器來監聽這個套接字通道。
   網上有一比較好的隱喻可以說明套接字的阻塞和非阻塞方法,摘抄如下:
一輛從 A 開往 B 的公共汽車上,路上有很多點可能會有人下車。司機不知道哪些點會有哪些人會下車,對於需要下車的人,如何處理更好?
1. 司機過程中定時詢問每個乘客是否到達目的地,若有人說到瞭,那麼司機停車,乘客下車。 ( 類似阻塞式 )
2. 每個人(相當於套接字通道)告訴售票員(相當於Selector)自己的目的地(相當於套接字的事件),然後睡覺,司機(相當於CPU)隻和售票員交互,到瞭某個點由售票員通知乘客下車。 ( 類似非阻塞 ),很顯然,每個人要到達某個目的地可以認為是一個線程,司機可以認為是 CPU 。在阻塞式裡面,每個線程需要不斷的輪詢,上下文切換,以達到找到目的地的結果。而在非阻塞方式裡,每個乘客 ( 線程 ) 都在睡覺 ( 休眠 ) ,隻在真正外部環境準備好瞭才喚醒,這樣的喚醒肯定不會阻塞。

 

發佈留言