IO NIO
面向流 面向緩沖
阻塞IO 非阻塞IO
無 selector
無 channel
Java NIO和IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩沖區的。Java IO面向流意味著每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。比如InputStream只能進行讀取操作,而OutputStream只能進行寫操作。Java NIO 提供 了channel,Channel和傳統IO中的Stream很相似。雖然很相似,但是有很大的區別,主要區別為:通道是雙向的,通過一個Channel既可以進行讀,也可以進行寫;它將數據讀取到一個它稍後處理的緩沖區,需要時可在緩沖區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩沖區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩沖區時,不要覆蓋緩沖區裡尚未處理的數據(如圖)。
Java IO的各種流是阻塞的。這意味著,當一個線程調用read()或write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閒時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。
一個常見的網絡IO通訊流程如下:
從該網絡通訊過程來理解一下何為阻塞:
在以上過程中若連接還沒到來,那麼accept會阻塞,程序運行到這裡不得不掛起,CPU轉而執行其他線程。
在以上過程中若數據還沒准備好,read會一樣也會阻塞。
阻塞式網絡IO的特點:多線程處理多個連接。每個線程擁有自己的棧空間並且占用一些CPU時間。每個線程遇到外部為准備好的時候,都會阻塞掉。阻塞的結果就是會帶來大量的進程上下文切換。且大部分進程上下文切換可能是無意義的。比如假設一個線程監聽一個端口,一天只會有幾次請求進來,但是該cpu不得不為該線程不斷做上下文切換嘗試,大部分的切換以阻塞告終。
一個常見的網絡NIO通訊流程如下:
把整個過程切換成小的任務,通過任務間協作完成。
由一個專門的線程來處理所有的IO事件,並負責分發。
事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。
線程通訊:線程之間通過wait,notify等方式通訊。保證每次上下文切換都是有意義的。減少無謂的進程切換。
Selector類是NIO的核心類,Selector能夠檢測多個注冊的通道上是否有事件發生,如果有事件發生,便獲取事件然後針對每個事件進行相應的響應處理。這樣一來,只是用一個單線程就可以管理多個通道,也就是管理多個連接。這樣使得只有在連接真正有讀寫事件發生時,才會調用函數來進行讀寫,就大大地減少了系統開銷,並且不必為每個連接都創建一個線程,不用去維護多個線程,並且避免了多線程之間的上下文切換導致的開銷。
與Selector有關的一個關鍵類是SelectionKey,一個SelectionKey表示一個到達的事件,這2個類構成了服務端處理業務的關鍵邏輯。
在前面已經提到,Channel和傳統IO中的Stream很相似。雖然很相似,但是有很大的區別,主要區別為:通道是雙向的,通過一個Channel既可以進行讀,也可以進行寫;而Stream只能進行單向操作,通過一個Stream只能進行讀或者寫;
以下是常用的幾種通道:
通過使用FileChannel可以從文件讀或者向文件寫入數據;通過SocketChannel,以TCP來向網絡連接的兩端讀寫數據;通過ServerSocketChanel能夠監聽客戶端發起的TCP連接,並為每個TCP連接創建一個新的SocketChannel來進行數據讀寫;通過DatagramChannel,以UDP協議來向網絡連接的兩端讀寫數據。
下面給出通過FileChannel來向文件中寫入數據的一個例子:
public
class
Test {
public
static
void
main(String[] args)
throws
IOException {
File file =
new
File(
"data.txt"
);
FileOutputStream outputStream =
new
FileOutputStream(file);
FileChannel channel = outputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(
1024
);
String string =
"java nio"
;
buffer.put(string.getBytes());
buffer.flip();
//此處必須要調用buffer的flip方法
channel.write(buffer);
channel.close();
outputStream.close();
}
}
通過上面的程序會向工程目錄下的data.txt文件寫入字符串"java nio",注意在調用channel的write方法之前必須調用buffer的flip方法,否則無法正確寫入內容。