上一節中咱們提到了同步異步與阻塞非阻塞的區別,知道了同步並不等於阻塞。而本節的主角NIO是一種同步非阻塞的I/O模型,而且是I/O多路複用模型。NIO在java中被稱爲 New I/O。它並不能提升I/O處理的效率,注意我這裏說的是效率,而從根本上解決的是I/O處理的併發問題。html
那麼NIO的本質是什麼樣的呢?它是怎樣與事件模型結合來解放線程、提升系統吞吐的呢?java
由上圖可知,全部的系統I/O都分爲兩個階段:等待數據和將數據從內核態複製到用戶態。編程
舉一個例子,傳統的BIO中,當咱們要讀某塊網卡接受到的網絡數據的時候,程序會一直阻塞直到有數據到來,在此階段cpu空轉不幹活。當監聽到有數據的時候,就將數據從內核緩存區copy到用戶緩存區,並且這個過程很是快,屬於memory copy,帶寬一般在1GB/s級別以上,能夠理解爲基本不耗時。緩存
理解I/O的這兩個階段實際意義尤爲的重要。下面講NIO以前,咱們先來深刻剖析一下傳統同步阻塞式BIO。服務器
下面這個僞代碼是一個傳統BIO模型,它的做用是打印客戶端發來的數據並返回數據。網絡
public class SocketServer { public static void main(String args[]) { ExecutorService executor = Executors.newFixedThreadPool(100);//線程池 try { ServerSocket ss = new ServerSocket(8888); System.out.println("啓動服務器...."); while (true) { //阻塞等待接受客戶端鏈接。 Socket socket = ss.accept(); System.out.println("客戶端:" + socket.getInetAddress().getLocalHost() + "已鏈接到服務器"); executor.submit(new DataHandler(socket)); } } catch (IOException e) { e.printStackTrace(); } } static class DataHandler implements Runnable { Socket socket; public DataHandler(Socket socket) { this.socket = socket; } @Override public void run() { //阻塞操做 try { BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String mess = br.readLine(); System.out.println("客戶端發來的數據:" + mess); //返回數據 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bw.write("服務器成功打印日誌\n"); bw.flush(); } catch (IOException e) { e.printStackTrace(); } } } }
上訴代碼中,總共有三處地方發生了阻塞,第一處是等待客戶端鏈接,第二處是input操做,第三處是output操做。因此該模型必須使用多線程來操做,若是是單線程,系統必將掛死在那裏。多線程
對應上述圖片,readLine()操做又有以下兩個階段(等待數據和將數據copy到用戶緩存中):
併發
這個模型嚴格的來講效率是最快的,注意,我說的是效率。可是這種模型有一個缺點就是每當一個客戶端發送請求的時候,服務器就會爲其建立一個線程,在活動鏈接數不是特別高(小於1000)的狀況下,這種模型是比較不錯的,可讓每個鏈接專一於本身的I/O而且編程模型簡單,也不用過多考慮系統的過載、限流等問題。線程池自己就是一個自然的漏斗,能夠緩衝一些系統處理不了的鏈接或請求。異步
可是這個方式的缺點就是,一旦有客戶端訪問,都建立一個專屬的線程去處理,即使有線程池的存在,當併發訪問量上來之後,CPU使用率會迅速上升,致使系統幾乎陷入不可用的狀態。socket
接下來咱們進入今天的主題:NIO。
若是是你在開發一個基於BIO模型的服務器,發現哪一天系統沒法抗住龐大的併發,那麼你有什麼手段去優化你的服務器呢?
沒錯,若是你看了以前的章節,那麼你的腦海必定會出現多路複用模型,在傳統BIO模型中,併發量上限的根本緣由就是啓動了過多的線程。
對於BIO模型,之因此須要多線程,是由於在進行I/O操做的時候,一是沒有辦法知道到底能不能寫、能不能讀,只能」傻等」,即便經過各類估算,算出來操做系統沒有能力進行讀寫,readLine()和write()函數中返回,這兩個函數沒法進行有效的中斷。因此除了多開線程另起爐竈,沒有好的辦法利用CPU。
NIO的讀寫函數則能夠馬上返回,這就給了咱們不開線程利用CPU的最好機會:若是一個鏈接不能讀寫(readLine()返回0或者write()返回0),咱們能夠把這件事記下來,記錄的方式一般是在Selector上註冊標記位,而後切換到其它就緒的鏈接(channel)繼續進行讀寫。
當一個客戶端請求到來的時候,咱們會將其(Channel)註冊到Selector上,而後Selector會不斷的輪詢註冊在其上的Channel,若是某個Channel上面發生了讀或者寫事件,這個Channel就會處於就緒狀態,會被Selector輪詢出來,而後經過調用方法獲取全部就緒Channel的集合,進行後續的操做。
一個多路複用器Selector能夠同時輪詢多個Channel,因爲JDK使用了epoll()(Netty基礎系列(1)中有介紹)代替傳統的select,因此沒有數量1024/2048的上限限制。這也就意味着每個線程負責Seletor的輪詢,就能夠接入成千上萬個客戶端,這確實是很是巨大的進步。
能夠將其想象成一個水管,一個客戶端的鏈接成功,能夠想象成這根水管一頭插入了服務器,一頭插入了客戶端,它們之間的通訊就靠的這根水管。
與傳統的流不一樣,流只能在一個方向是移動(如上述代碼,input只能寫入,output只能寫出)。可是Channel是全雙工的,意思是能同時支持讀寫操做。
在NIO庫類中加入了一個Buffer對象。它區別於傳統的流,能寫入或者將數據直接讀到Stream對象中。NIO全部數據都是基於Buffer處理的,在讀取數據的時候直接讀取Buffer裏的數據,寫數據的時候直接往Buffer裏寫數據。任什麼時候候訪問NIO中的數據,都是經過緩衝區進行操做的。
一般狀況下,操做系統的一次寫操做分爲兩步: 1. 將數據從用戶空間拷貝到系統空間。 2. 從系統空間往網卡寫。同理,讀操做也分爲兩步: ① 將數據從網卡拷貝到系統空間; ② 將數據從系統空間拷貝到用戶空間。
可是值得注意的是,若是使用了DirectByteBuffer(繼承Buffer),通常來講能夠減小一次系統空間到用戶空間的拷貝。但Buffer建立和銷燬的成本更高,更不宜維護,一般會用內存池來提升性能。
若是數據量比較小的中小應用狀況下,能夠考慮使用heapBuffer;反之能夠用directBuffer。
本章多個角度的解釋了NIO,以及NIO的基本組件。
NIO編程的代碼博主沒有過多的解釋,由於對於NIO編程博主也是個小菜雞。可是!Netty將NIO進行了進一步的封裝,讓咱們能使用更簡單,更高效的API來完成咱們NIO操做。比直接寫NIO更輕鬆,也沒必要在乎操做系統之間的區別。可是有興趣的小夥伴能夠自行學習NIO編程,而後再體會對比一下與Netty編程實現相同功能的難度與代碼量。你就會深深感嘆,Netty真他麼的強大。
最後再提醒各位一點,使用NIO != 高性能,當鏈接數<1000,併發程度不高或者局域網環境下NIO並無顯著的性能優點。