前面的一些文章中我總結了一些Java IO和NIO相關的主要知識點,也是管中窺豹,IO類庫已經功能很強大了,但是Java 為什么又要引入NIO,這是我一直不是很清楚的?前面也只是簡單提及了一下:因為性能,但是僅僅是因為性能嗎,除此之外是否還有別的原因,或者說既然NIO性能好,那為什么現在我們還在使用IO。本節我們就來詳細對比一下兩者的特性以及兩者之間的不一致對我們編碼所帶來的影響。
同樣,本文會主要圍繞下面幾個方面來總結:
1. Java NIO和IO的主要區別
兩者之間的不同主要體現在如下三個方面:
- Java IO是面向流(Stream)的,而Java NIO是面向緩沖區(Buffer)的;
- IO模型的不同,Java IO是屬於阻塞式IO(Blocking IO),而Java NIO是屬於非阻塞式IO(Non Blocking IO);
- Java NIO中還引入了Selector的概念,可以實現多路復用;
在接下來的部分,我們逐個討論這三個不同。
1.1 面向流與面向緩沖區
Java NIO和IO之間第一個不同點是IO是面向流(Stream)的而NIO是面向緩沖區(Buffer)的。
Java IO是面向流的,這意味着是一次性從流中讀取一批數據,這些數據並不會緩存在任何地方,並且對於在流中的數據是不支持在數據中前后移動。如果需要在這些數據中移動(為什么要移動,可以多次讀取),則還是需要將這部分數據先緩存在緩沖區中。
而Java NIO采用的是面向緩沖區的方式,有些不同,數據會先讀取到緩沖區中以供稍后處理。在buffer中是可以方便地前移和后移,這使得在處理數據時可以有更大的靈活性。但是呢需要檢查buffer是否包含需要的所有數據以便能夠將其完整地處理,並且需要確保在通過channel往buffer讀數據的時候不能夠覆蓋還未處理的數據。
1.2 IO模型的區別
Java IO中使用的流是屬於阻塞式的,意味着當線程調用其read()或write()方法時線程會阻塞,直到完成了數據的讀寫,在讀寫的過程中線程是什么都做不了的。
Java NIO提供了一種非阻塞模式,使得線程向channel請求讀數據時,只會獲取已經就緒的數據,並不會阻塞以等待所有數據都准備好(IO就是這樣做),這樣在數據准備的階段線程就能夠去處理別的事情。對於非阻塞式寫數據是一樣的。線程往channel中寫數據時,並不會阻塞以等待數據寫完,而是可以處理別的事情,等到數據已經寫好了,線程再處理這部分事情。
當線程在進行IO調用並且不會進入阻塞的情況下,這部分的空余時間就可以花在和其他channel進行IO交互上。也就是說,這樣單個線程就能夠管理多個channel的輸入和輸出了。
1.3 Selector
Java NIO中的Selector允許單個線程監控多個channel,可以將多個channel注冊到一個Selector中,然后可以"select"出已經准備好數據的channel,或者准備好寫入的channel。這個selector機制使得單個線程同時管理多個channel變得更容易。
2. NIO和IO的不同對代碼設計帶來的變化
選擇使用NIO還是IO作為開發工具包會在如下幾個方面影響應用設計:
- API是調用NIO類庫還是IO類庫;
- 數據的處理方式;
- 用來處理數據的線程的數量;
2.1 API的調用
采用NIO的API調用方式和IO是不一樣的,與直接從InputStream中讀取字節數據不同,在NIO中,數據必須要先被讀到buffer中,然后再從那里進行后續的處理。
2.2 數據的處理方式
采用NIO的設計還是IO的設計,數據的處理方式也是不一樣的。
在IO設計中是從InputStream或Reader中逐字節讀取數據。在下面例子中,我們通過一個處理基於文本的簡單例子來說明兩種設計的區別:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
采用IO的方式,這些數據流會像下面這樣處理:
InputStream input = ... ; // get the InputStream from the client socket BufferedReader reader = new BufferedReader(new InputStreamReader(input)); String nameLine = reader.readLine(); String ageLine = reader.readLine(); String emailLine = reader.readLine(); String phoneLine = reader.readLine();
注意在這里處理狀態是通過程序執行了多少就能夠確定的。換句話說,當第一行reader.readLine()返回之后,可以確定已經讀了一整行。因為readLine()會阻塞直到整行數據讀完。而且我們能夠確切地知道所讀取的這第一行是包含名字的。類似,第二次調用readLine()返回之后我們確切地知道所讀取的內容包含年齡。
可以知道,上面的程序只有當有新的數據是可讀時才會進行處理,在每一步都知道數據是什么。一旦執行讀寫的線程已經讀取了一些數據之后,是不能夠再返回到前面的數據(因為流的方式只能讀取一次,很好理解,像水一樣,流完了就流完了,除非你把它裝到容器里面)。上面程序中所遵循的原則如下圖所示:
而NIO的實現則看起來有些不同,如下:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer);
注意第二行是從channel讀取數據到buffer中,當read()方法返回時我們是不知道是否所有需要的數據有沒有全部讀到buffer中,我們知道的只是buffer中可能包含一部分數據,這會使得整個過程的處理有點麻煩。
假設,在第一次調用read()之后,所有讀到buffer中的數據只有半行,比如,"Name:An"。這時可以處理數據嗎,顯然是不可以的(因為還沒有讀完),需要等到至少一行數據被讀到buffer中。
那么我們又如何來知道buffer中包含足夠可以處理的數據呢?唯一的辦法只有檢查buffer中的數據了。所以結果就是我們需要通過多次檢查buffer中的數據來判斷數據是否已經全部讀進buffer了。這樣就很低效,而且容易導致程序設計混亂。比如:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer); while(! bufferFull(bytesRead) ) { bytesRead = inChannel.read(buffer); }
bufferFull()方法會跟蹤有多少數據被讀到buffer中了,並且返回true或者false,取決於buffer是否已滿。換言之,如果buffer中的數據已經可供處理,那就代表它已經滿了。
bufferFull()方法會掃描整個buffer,要保證掃描並不會影響整個buffer的狀態,不然可能導致后面要讀入buffer中的數據不能讀到正確地位置。這並非不可能,所以對於設計者來說這是一個需要關注的地方。
如果buffer已滿,那其中的數據就可供處理。如果沒滿,那可能需要部分地處理那些數據(如果需要的話),只是在大部分場景下是不需要的。
下圖描述了這種 is-data-in-buffer-ready的循環:
3. 兩種IO的各自適用場景
NIO使得通過單個或少量線程來管理多個channel(網絡連接或者文件)成為可能,但是代價是傳遞數據會比從阻塞的流中讀數據更復雜。我們學習一項新的技術時,既要看到其優點也要看到其缺點。
如果需要同時管理數以千計的連接,而且每個連接只會發送少量的數據,比如聊天服務器,用NIO的方式來實現這個服務器則比較合適。類似的,如果需要長時間保持一些和別的電腦的連接,比如在一個P2P網絡中,用單個線程來管理所有的對外連接也有優勢。如下圖描述了這種單個線程,多個連接的設計模型:
如果只有少量的連接,但是每個連接又都占用大量的帶寬,短時間之內發送大量數據,這時后也許傳統的IO模型會更適用,因為專一,所以在特定場景下可以更高效。如下圖描述了一個基於傳統IO模型設計的服務器模型:
4. 總結
在前面總結了很多IO和NIO的相關知識之后,本文總結了Java中兩種IO類庫的區別即各自的優缺點:
- 傳統Java IO是面向流,從流中讀取數據或者寫入到流中,而Java NIO是面向緩沖區的,通過channel和buffer的搭配使用來讀取或者寫入數據;
- 面向流只能一次讀取數據;面向緩沖區可以多次讀取數據;
- 面向流的方式處理數據過程相對簡單,易於實現;而Java NIO中面向buffer的方式一般是非阻塞的方式,所以在數據的操作上會更復雜,從而會增加代碼的復雜程度;
- Java NIO提供了Selector的概念,可以通過少量線程處理多個連接,可以有效處理並發;而Java IO則專注於單個線程阻塞式讀寫,對於少量連接但是每個連接都占用大量寬帶的場景更適用;
技術沒有好壞,只有合適與否!