前文中我們總結了linux系統中的5中IO模型,並且着重介紹了其中的4種IO模型:
- 阻塞I/O(blocking IO)
- 非阻塞I/O(nonblocking IO)
- I/O多路復用(IO multiplexing)
- 異步I/O(asynchronous IO)
但是前面總結的IO模型只是限定在linux下,更偏向於操作系統底層的概念,並沒有涉及到Java應用層面,其實Java中也提供了和前面操作系統層面的IO模型相對應的概念,這是本文接下來要講的重點。
同樣本文會圍繞如下幾點進行展開:
1. I/O模型在Java中的對應
1.1 阻塞I/O
傳統Java IO提供的面向流的IO操作方式就屬於阻塞式的,調用其read()或write()方法的線程會阻塞,直到完成了數據的讀寫,在讀寫的過程中線程是什么都做不了的。
1.2 非阻塞I/O
Java NIO類庫提供了多種支持非阻塞模式的類,比如Channel、Buffer,可以將其設置為非阻塞模式,線程向channel請求讀數據時,只會獲取已經就緒的數據,並不會阻塞以等待所有數據都准備好,這樣在數據准備的階段線程就能夠去處理別的事情,這就是非阻塞式讀,對於寫數據是一樣的。
這里和上面阻塞的區別就是,調用read()或write()方法並不阻塞,而是會立即返回,但是這時候IO操作往往是還沒有結束的。
1.3 多路復用
Java NIO中的Selector允許單個線程監控多個channel,可以將多個channel注冊到一個Selector中,然后可以"select"出已經准備好數據的channel,或者准備好寫入的channel,然后對其進行讀或者寫數據,這就是多路復用。
1.4 異步IO
異步IO模型是比較理想的IO模型,在異步IO模型中,當用戶線程發起read操作之后,立刻就可以開始去做其它的事。另一方面,內核會等待數據准備完成,然后將數據復制到用戶線程,當這一切都完成之后,內核會給用戶線程發送一個信號,告訴它read操作完成了。也就是說用戶線程完全不需要關心實際的整個IO操作了,只需要發起請求就行了,當收到內核的成功信號時就可以直接去使用數據了。這就是和非阻塞式的區別,如果說阻塞式IO是完全手動,非阻塞式IO就是半自動,而異步IO就是全自動,多路復用呢?我覺得可以是半自動沖鋒槍^_^
在Java 7中,提供了Asynchronous IO,Java NIO中的AsynchronousFileChannel支持異步模型實現的。
2. 適用場景
BIO方式適用於連接數目比較小且每個連接占用大量寬帶,這種方式對服務器資源要求比較高,JDK1.4以前的唯一選擇,但程序直觀簡單易理解。
NIO方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,編程比較復雜,JDK1.4開始支持。
AIO方式適用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與並發操作,編程比較復雜,JDK7開始支持。
3. Java中各種IO模型的使用方式
前面講了這么多,即講了linux下的IO模型,又講了Java中對這些IO模型的支持,到這里我覺得是時候找一些Java中實際的例子看看,下面就分別用三種IO模型來讀寫文件。
3.1 通過BIO方式讀寫文件
public void rwByBIO() { BufferedReader br = null; FileInputStream in = null; FileOutputStream out = null; try { in = new FileInputStream("test.txt"); out = new FileOutputStream("testBIO.txt"); List<Integer> list = new ArrayList(); int temp; while((temp = in.read()) != -1) { out.write(temp); } br = new BufferedReader(new InputStreamReader(new FileInputStream("testBIO.txt"))); System.out.println(br.readLine()); }catch(Exception e) { e.printStackTrace(); }finally { if(br != null) { try { br.close(); }catch(IOException e) { e.printStackTrace(); } } if(out != null) { try { out.close(); }catch(IOException e) { e.printStackTrace(); } } } }
在根目錄下准備好文件test.txt,里面寫上准備好的內容,比如"黃沙百戰穿金甲,不破樓蘭終不還",然后跑起來,之后應該會多出一個文件testBIO.txt,里面內容是一樣的。我們通過BIO的方式讀取test.txt中的內容,同樣以BIO的方式寫入到testBIO.txt中。
3.2 通過NIO讀寫文件
public void rwByNIO() { FileChannel readChannel = null; FileChannel writeChannel = null; try { readChannel = new RandomAccessFile(new File("test.txt"),"r").getChannel(); writeChannel = new RandomAccessFile(new File("testNIO.txt"),"rw").getChannel(); ByteBuffer buffer = ByteBuffer.allocate(10); int bytesRead = readChannel.read(buffer); while(bytesRead != -1) { buffer.flip(); while(buffer.hasRemaining()) { // 寫入文件 writeChannel.write(buffer); } // 一次寫完之后 buffer.clear(); bytesRead = readChannel.read(buffer); } }catch(Exception e) { e.printStackTrace(); }finally { if(readChannel != null) { try { readChannel.close(); } catch (IOException e) { e.printStackTrace(); } } if(writeChannel != null) { try { writeChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } }
這里是通過NIO中的FileChannel來讀寫文件,但是要注意,雖然這一節的標題是說用NIO的方式來讀寫文件,但是FileChannel並不支持非阻塞模式,所以其實際上還是屬於阻塞的,即BIO的方式,只是因為這里為了統一演示讀寫文件的例子,所以仍然使用NIO中的FileChannel類來完成。
3.3 通過AIO方式讀寫文件
public void rwByAIO() { Path path = Paths.get("test.txt"); AsynchronousFileChannel fileChannel = null; try { fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); ByteBuffer buffer = ByteBuffer.allocate(1024); long position = 0; Future<Integer> operation = fileChannel.read(buffer, position); while(!operation.isDone()); buffer.flip(); Path writePath = Paths.get("testAIO.txt"); if(!Files.exists(writePath)){ Files.createFile(writePath); } AsynchronousFileChannel writeFileChannel = AsynchronousFileChannel.open(writePath, StandardOpenOption.WRITE); writeFileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("bytes written: " + result); } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println("Write failed"); exc.printStackTrace(); } }); }catch(Exception e) { e.printStackTrace(); } }
這個例子中是通過異步地方式來讀寫文件。當調用了Java NIO中的AsynchronousFileChannel對這種操作提供了支持,當調用其read()方法時會立即返回一個Future對象,通過調用其isDone方法來得知數據是否讀取完畢。
4. 總結
本文結合前文講到的IO模型,分別對應到Java中的具體類庫實現,並通過例子演示了BIO、NIO、AIO三種方式讀寫文件。
- 標准Java IO提供的面向流的方式屬於BIO模型的實現,在讀取的過程中是會阻塞的;
- Java NIO提供的Channel和Buffer是支持NIO模式的,調用了Channel的讀寫方法之后可以立即返回,在往Buffer中准備數據的過程中是不阻塞的,線程可以做別的事情,但是從Buffer讀寫數據是阻塞的;
- Java NIO中提供的AsynchronousFileChannel支持異步讀寫文件,當調用了其讀寫方法之后可以立即返回,只需要等待系統把數據復制到指定位置即可,整個過程都不會阻塞;