什麼是 NIO? NIO 和 BIO、AIO 之間的區別是什麼?NIO主要用來解決什麼問題?面試問題?1 BIO,NIO,AIO都有什麼區別,NIO的原理是什麼?BIO
BIO:傳統的網路通訊模型,就是BIO,同步阻塞IO, 其實就是服務端建立一個ServerSocket, 然後就是客戶端用一個Socket去連線服務端的那個ServerSocket, ServerSocket接收到了一個的連線請求就建立一個Socket和一個執行緒去跟那個Socket進行通訊。接著客戶端和服務端就進行阻塞式的通訊,客戶端傳送一個請求,服務端Socket進行處理後返回響應,在響應返回前,客戶端那邊就阻塞等待,什麼事情也做不了。 這種方式的缺點, 每次一個客戶端接入,都需要在服務端建立一個執行緒來服務這個客戶端,這樣大量客戶端來的時候,就會造成服務端的執行緒數量可能達到了幾千甚至幾萬,這樣就可能會造成服務端過載過高,最後崩潰死掉。————————————————BIO模型圖:
Acceptor:
傳統的IO模型的網路服務的設計模式中有兩種比較經典的設計模式: 一個是多執行緒, 一種是依靠執行緒池來進行處理。如果是基於多執行緒的模式來的話,就是這樣的模式,這種也是
Acceptor執行緒模型。
NIONIO: NIO是一種同步非阻塞IO, 基於Reactor模型來實現的。其實相當於就是一個執行緒處理大量的客戶端的請求,透過一個執行緒輪詢大量的channel,每次就獲取一批有事件的channel,然後對每個請求啟動一個執行緒處理即可。這裡的核心就是非阻塞,就那個selector一個執行緒就可以不停輪詢channel,所有客戶端請求都不會阻塞,直接就會進來,大不了就是等待一下排著隊而已。這裡面最佳化BIO的核心就是,一個客戶端並不是時時刻刻都有資料進行互動,沒有必要死耗著一個執行緒不放,所以客戶端選擇了讓執行緒歇一歇,只有客戶端有相應的操作的時候才發起通知,建立一個執行緒來處理請求。————————————————NIO:模型圖
Reactor模型:
AIOAIO:非同步非阻塞IO,基於Proactor模型實現。 每個連線傳送過來的請求,都會繫結一個Buffer,然後通知作業系統去完成非同步的讀,這個時間你就可以去做其他的事情,等到作業系統完成讀之後,就會呼叫你的介面,給你作業系統非同步讀完的資料。這個時候你就可以拿到資料進行處理,將資料往回寫,在往回寫的過程,同樣是給作業系統一個Buffer,讓作業系統去完成寫,寫完了來通知你。這兩個過程都有buffer存在,資料都是透過buffer來完成讀寫。
這裡面的主要的區別在於將資料寫入的緩衝區後,就不去管它,剩下的去交給作業系統去完成。作業系統寫回資料也是一樣,寫到Buffer裡面,寫完後通知客戶端來進行讀取資料。————————————————AIO:模型圖
聊完了BIO,NIO,AIO的區別之後,現在我們再結合這三個模型來說下同步和阻塞的一些問題。
2 各種阻塞解釋同步阻塞同步阻塞:為什麼說BIO是同步阻塞的呢?其實這裡說的不是針對網路通訊模型而言,而是針對磁碟檔案讀寫IO操作來說的。因為用BIO的流讀寫檔案,例如FileInputStrem,是說你發起個IO請求直接hang死,卡在那裡,必須等著搞完了這次IO才能返回。
同步非阻塞:同步非阻塞:為什麼說NIO為啥是同步非阻塞?因為無論多少客戶端都可以接入服務端,客戶端接入並不會耗費一個執行緒,只會建立一個連線然後註冊到selector上去罷了,你就可以去幹其他你想幹的其他事情了, 一個selector執行緒不斷的輪詢所有的socket連線,發現有事件了就通知你,然後你就啟動一個執行緒處理一個請求即可,這個過程的話就是非阻塞的。但是這個處理的過程中,你還是要先讀取資料,處理,再返回的,這是個同步的過程。
非同步非阻塞非同步非阻塞:為什麼說AIO是非同步非阻塞?透過AIO發起個檔案IO操作之後,你立馬就返回可以幹別的事兒了,接下來你也不用管了,作業系統自己幹完了IO之後,告訴你說ok了, 當你基於AIO的api去讀寫檔案的時候, 當你發起一個請求之後,剩下的事情就是交給了作業系統,當讀寫完成後, 作業系統會來回調你的介面, 告訴你操作完成, 在這期間不需要等待, 也不需要去輪詢判斷作業系統完成的狀態,你可以去幹其他的事情。 同步就是自己還得主動去輪詢作業系統,非同步就是作業系統反過來通知你。所以來說, AIO就是非同步非阻塞的。
3 NIO核心元件詳細講解學習NIO先來搞清楚一些相關的概念,NIO通訊有哪些相關元件,對應的作用都是什麼,之間有哪些聯絡?
多路複用機制實現Selector首先我們來了解下傳統的Socket網路通訊模型。
傳統Socket通訊原理圖
為什麼傳統的socket不支援海量連線每次一個客戶端接入,都是要在服務端建立一個執行緒來服務這個客戶端的,這會導致大量的客戶端的時候,服務端的執行緒數量可能達到幾千甚至幾萬,幾十萬,這會導致伺服器端程式負載過高,不堪重負,最終系統崩潰死掉。
接著來看下NIO是如何基於Selector實現多路複用機制支援的海量連線。NIO原理圖
多路複用機制是如何支援海量連線NIO的執行緒模型 對Socket發起的連線不需要每個都建立一個執行緒,完全可以使用一個Selector來多路複用監聽N多個Channel是否有請求,該請求是對應的連線請求,還是傳送資料的請求,這裡面是基於作業系統底層的Select通知機制的,一個Selector不斷的輪詢多個Channel,這樣避免了建立多個執行緒,只有當莫個Channel有對應的請求的時候才會建立執行緒,可能說1000個請求, 只有100個請求是有資料互動的, 這個時候可能server端就提供10個執行緒就能夠處理這些請求。這樣的話就可以避免了建立大量的執行緒。
NIO如何透過Buffer來緩衝資料的NIO中的Buffer是個什麼東西 ?
學習NIO,首當其衝就是要了解所謂的Buffer緩衝區,這個東西是NIO裡比較核心的一個部分,一般來說,如果你要透過NIO寫資料到檔案或者網路,或者是從檔案和網路讀取資料出來此時就需要透過Buffer緩衝區來進行。Buffer的使用一般有如下幾個步驟:
寫入資料到Buffer,呼叫flip()方法,從Buffer中讀取資料,呼叫clear()方法或者compact()方法。
Buffer中對應的Position, Mark, Capacity,Limit都啥?capacity: 緩衝區容量的大小,就是裡面包含的資料大小。limit: 對buffer緩衝區使用的一個限制,從這個index開始就不能讀取資料了。position: 代表著陣列中可以開始讀寫的index, 不能大於limit。mark: 是類似路標的東西,在某個position的時候,設定一下mark,此時就可以設定一個標記,後續呼叫reset()方法可以把position復位到當時設定的那個mark上去,把position或limit調整為小於mark的值時,就丟棄這個mark。如果使用的是Direct模式建立的Buffer的話,就會減少中間緩衝直接使用的是DirectorBuffer來進行資料的儲存。————————————————
如何透過Channel和FileChannel讀取Buffer資料寫入磁碟的NIO中,Channel是什麼?
Channel是NIO中的資料通道,類似流,但是又有些不同,Channel即可從中讀取資料,又可以從寫資料到通道中,但是流的讀寫通常是單向的。Channel可以非同步的讀寫。Channel中的資料總是要先讀到一個Buffer中,或者從緩衝區中將資料寫到通道中。
FileChannel的作用是什麼 Buffer有不同的型別,同樣Channel也有好幾個型別。 FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel。這些通道涵蓋了UDP 和 TCP 網路IO,以及檔案IO。而FileChannel就是檔案IO對應的管道, 在讀取檔案的時候會用到這個管道。
4 問題(程式碼實現)BIO 和 NIO 作為 Server 端,當建立了 10 個連線時,分別產生多少個執行緒?
答案: 因為傳統的 IO 也就是 BIO 是同步執行緒堵塞的,所以每個連線都要分配一個專用執行緒來處理請求,這樣 10 個連線就會建立 10 個執行緒去處理。而 NIO 是一種同步非阻塞的 I/O 模型,它的核心技術是多路複用,可以使用一個連結上的不同通道來處理不同的請求,所以即使有 10 個連線,對於 NIO 來說,開啟 1 個執行緒就夠了。————————————————
BIO 程式碼實現publicclassDemoServerextendsThread{privateServerSocket serverSocket;publicint getPort(){return serverSocket.getLocalPort();}publicvoid run(){try{serverSocket =newServerSocket(0);while(true){Socket socket = serverSocket.accept();RequestHandler requestHandler =newRequestHandler(socket);requestHandler.start();}}catch(IOException e){e.printStackTrace();}finally{if(serverSocket !=null){try{serverSocket.close();}catch(IOException e){e.printStackTrace();}}}}publicstaticvoid main(String[] args)throwsIOException{DemoServer server =newDemoServer();server.start();try(Socket client =newSocket(InetAddress.getLocalHost(), server.getPort())){BufferedReader bufferedReader =newBufferedReader(newInputStreamReader(client.getInputStream()));bufferedReader.lines().forEach(s ->System.out.println(s));}}}// 簡化實現,不做讀取,直接傳送字串classRequestHandlerextendsThread{privateSocket socket;RequestHandler(Socket socket){this.socket = socket;}@Overridepublicvoid run(){try(PrintWriter out =newPrintWriter(socket.getOutputStream());){out.println("Hello world!");out.flush();}catch(Exception e){e.printStackTrace();}}}
伺服器端啟動 ServerSocket,埠 0 表示自動繫結一個空閒埠。呼叫 accept 方法,阻塞等待客戶端連線。利用 Socket 模擬了一個簡單的客戶端,只進行連線、讀取、列印。當連線建立後,啟動一個單獨執行緒負責回覆客戶端請求。這樣,一個簡單的 Socket 伺服器就被實現出來了。————————————————
NIO 程式碼實現publicclassNIOServerextendsThread{publicvoid run(){try(Selector selector =Selector.open();ServerSocketChannel serverSocket =ServerSocketChannel.open();){// 建立 Selector 和 ChannelserverSocket.bind(newInetSocketAddress(InetAddress.getLocalHost(),8888));serverSocket.configureBlocking(false);// 註冊到 Selector,並說明關注點serverSocket.register(selector,SelectionKey.OP_ACCEPT);while(true){selector.select();// 阻塞等待就緒的 Channel,這是關鍵點之一Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> iter = selectedKeys.iterator();while(iter.hasNext()){SelectionKey key = iter.next();// 生產系統中一般會額外進行就緒狀態檢查sayHelloWorld((ServerSocketChannel) key.channel());iter.remove();}}}catch(IOException e){e.printStackTrace();}}privatevoid sayHelloWorld(ServerSocketChannel server)throwsIOException{try(SocketChannel client = server.accept();){ client.write(Charset.defaultCharset().encode("Hello world!"));}}// 省略了與前面類似的 main}
首先,透過 Selector.open() 建立一個 Selector,作為類似排程員的角色。然後,建立一個 ServerSocketChannel,並且向 Selector 註冊,透過指定 SelectionKey.OP_ACCEPT,告訴排程員,它關注的是新的連線請求。注意:為什麼我們要明確配置非阻塞模式呢?這是因為阻塞模式下,註冊操作是不允許的,會丟擲 IllegalBlockingModeException 異常。Selector 阻塞在 select 操作,當有 Channel 發生接入請求,就會被喚醒。在 sayHelloWorld 方法中,透過 SocketChannel 和 Buffer 進行資料操作,在本例中是傳送了一段字串。可以看到,在前面兩個樣例中,IO 都是同步阻塞模式,所以需要多執行緒以實現多工處理。而 NIO 則是利用了單執行緒輪詢事件的機制,透過高效地定位就緒的 Channel,來決定做什麼,僅僅 select 階段是阻塞的,可以有效避免大量客戶端連線時,頻繁執行緒切換帶來的問題,應用的擴充套件能力有了非常大的提高。
6 NIO與IO的區別NIO即New IO,這個庫是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但實現方式不同,NIO主要用到的是塊,所以NIO的效率要比IO高很多。在Java API中提供了兩套NIO,一套是針對標準輸入輸出NIO,另一套就是網路程式設計NIO。 NIO和IO的主要區別,下表總結了Java IO和NIO之間的主要區別:————————————————
1、面向流與面向緩衝 Java IO和NIO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。 Java IO面向流意味著每次從流中讀一個或多個位元組,直至讀取所有位元組,它們沒有被快取在任何地方。此外,它不能前後移動流中的資料。如果需要前後移動從流中讀取的資料,需要先將它快取到一個緩衝區。Java NIO的緩衝導向方法略有不同。資料讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的資料。而且,需確保當更多的資料讀入緩衝區時,不要覆蓋緩衝區裡尚未處理的資料。
2、阻塞與非阻塞IO Java IO的各種流是阻塞的。這意味著,當一個執行緒呼叫read() 或 write() 時,該執行緒被阻塞,直到有一些資料被讀取,或資料完全寫入。該執行緒在此期間不能再幹任何事情了。Java NIO的非阻塞模式,使一個執行緒從某通道傳送請求讀取資料,但是它僅能得到目前可用的資料,如果目前沒有資料可用時,就什麼都不會獲取,而不是保持執行緒阻塞,所以直至資料變的可以讀取之前,該執行緒可以繼續做其他的事情。非阻塞寫也是如此。一個執行緒請求寫入一些資料到某通道,但不需要等待它完全寫入,這個執行緒同時可以去做別的事情。執行緒通常將非阻塞IO的空閒時間用於在其它通道上執行IO操作,所以一個單獨的執行緒現在可以管理多個輸入和輸出通道(channel)。
3、選擇器(Selectors) Java NIO的選擇器允許一個單獨的執行緒來監視多個輸入通道,你可以註冊多個通道使用一個選擇器,然後使用一個單獨的執行緒來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的執行緒很容易來管理多個通道。
7 NIO和IO適用場景NIO是為彌補傳統IO的不足而誕生的,但是尺有所短寸有所長,**NIO也有缺點,因為NIO是面向緩衝區的操作,每一次的資料處理都是對緩衝區進行的,那麼就會有一個問題,在資料處理之前必須要判斷緩衝區的資料是否完整或者已經讀取完畢,如果沒有,假設資料只讀取了一部分,那麼對不完整的資料處理沒有任何意義。**所以每次資料處理之前都要檢測緩衝區資料。 那麼NIO和IO各適用的場景是什麼呢? 如果需要管理同時開啟的成千上萬個連線,這些連線每次只是傳送少量的資料,例如聊天伺服器,這時候用NIO處理資料可能是個很好的選擇。 而如果只有少量的連線,而這些連線每次要傳送大量的資料,這時候傳統的IO更合適。使用哪種處理資料,需要在資料的響應等待時間和檢查緩衝區資料的時間上作比較來權衡選擇。