熟練掌握 BIO,NIO,AIO 的基本概念以及一些常見問題是你準備面試的過程中不可或缺的一部分,另外這些知識點也是你學(xué)習(xí) Netty 的基礎(chǔ)。
BIO,NIO,AIO 總結(jié)
Java 中的 BIO、NIO和 AIO 理解為是 Java 語言對操作系統(tǒng)的各種 IO 模型的封裝。程序員在使用這些 API 的時候,不需要關(guān)心操作系統(tǒng)層面的知識,也不需要根據(jù)不同操作系統(tǒng)編寫不同的代碼。只需要使用Java的API就可以了。 在講 BIO,NIO,AIO 之前先來回顧一下這樣幾個概念:同步與異步,阻塞與非阻塞。同步與異步
同步:?同步就是發(fā)起一個調(diào)用后,被調(diào)用者未處理完請求之前,調(diào)用不返回。
異步:?異步就是發(fā)起一個調(diào)用后,立刻得到被調(diào)用者的回應(yīng)表示已接收到請求,但是被調(diào)用者并沒有返回結(jié)果,此時我們可以處理其他的請求,被調(diào)用者通常依靠事件,回調(diào)等機制來通知調(diào)用者其返回結(jié)果。 同步和異步的區(qū)別最大在于異步的話調(diào)用者不需要等待處理結(jié)果,被調(diào)用者會通過回調(diào)等機制來通知調(diào)用者其返回結(jié)果。阻塞和非阻塞
阻塞:?阻塞就是發(fā)起一個請求,調(diào)用者一直等待請求結(jié)果返回,也就是當前線程會被掛起,無法從事其他任務(wù),只有當條件就緒才能繼續(xù)。
非阻塞:?非阻塞就是發(fā)起一個請求,調(diào)用者不用一直等著結(jié)果返回,可以先去干其他事情。
那么同步阻塞、同步非阻塞和異步非阻塞又代表什么意思呢?舉個生活中簡單的例子,你媽媽讓你燒水,小時候你比較笨啊,在哪里傻等著水開(同步阻塞?)。等你稍微再長大一點,你知道每次燒水的空隙可以去干點其他事,然后只需要時不時來看看水開了沒有(同步非阻塞?)。后來,你們家用上了水開了會發(fā)出聲音的壺,這樣你就只需要聽到響聲后就知道水開了,在這期間你可以隨便干自己的事情,你需要去倒水了(異步非阻塞?)。
1. BIO (Blocking I/O)
同步阻塞I/O模式,數(shù)據(jù)的讀取寫入必須阻塞在一個線程內(nèi)等待其完成。
1.1 傳統(tǒng) BIO
BIO通信(一請求一應(yīng)答)模型圖如下(圖源網(wǎng)絡(luò),原出處不明):

采用 BIO 通信模型?的服務(wù)端,通常由一個獨立的 Acceptor 線程負責監(jiān)聽客戶端的連接。我們一般通過在while(true)?循環(huán)中服務(wù)端會調(diào)用accept()
方法等待接收客戶端的連接的方式監(jiān)聽請求,請求一旦接收到一個連接請求,就可以建立通信套接字在這個通信套接字上進行讀寫操作,此時不能再接收其他客戶端連接請求,只能等待同當前連接的客戶端的操作執(zhí)行完成, 不過可以通過多線程來支持多個客戶端的連接,如上圖所示。
如果要讓BIO 通信模型?能夠同時處理多個客戶端請求,就必須使用多線程(主要原因是socket.accept()?、socket.read()?、socket.write()?涉及的三個主要函數(shù)都是同步阻塞的),也就是說它在接收到客戶端連接請求之后為每個客戶端創(chuàng)建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應(yīng)答給客戶端,線程銷毀。這就是典型的一請求一應(yīng)答通信模型?。我們可以設(shè)想一下如果這個連接不做任何事情的話就會造成不必要的線程開銷,不過可以通過線程池機制改善,線程池還可以讓線程的創(chuàng)建和回收成本相對較低。使用FixedThreadPool?可以有效的控制了線程的最大數(shù)量,保證了系統(tǒng)有限的資源的控制,實現(xiàn)了N(客戶端請求數(shù)量):M(處理客戶端請求的線程數(shù)量)的偽異步I/O模型(N 可以遠遠大于 M),下面一節(jié)"偽異步 BIO"中會詳細介紹到。
我們再設(shè)想一下當客戶端并發(fā)訪問量增加后這種模型會出現(xiàn)什么問題?
在 Java 虛擬機中,線程是寶貴的資源,線程的創(chuàng)建和銷毀成本很高,除此之外,線程的切換成本也是很高的。尤其在 Linux 這樣的操作系統(tǒng)中,線程本質(zhì)上就是一個進程,創(chuàng)建和銷毀線程都是重量級的系統(tǒng)函數(shù)。如果并發(fā)訪問量增加會導(dǎo)致線程數(shù)急劇膨脹可能會導(dǎo)致線程堆棧溢出、創(chuàng)建新線程失敗等問題,最終導(dǎo)致進程宕機或者僵死,不能對外提供服務(wù)。
1.2 偽異步 IO
為了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題,后來有人對它的線程模型進行了優(yōu)化一一一后端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數(shù)M:線程池最大線程數(shù)N的比例關(guān)系,其中M可以遠遠大于N.通過線程池可以靈活地調(diào)配線程資源,設(shè)置線程的最大值,防止由于海量并發(fā)接入導(dǎo)致線程耗盡。
偽異步IO模型圖(圖源網(wǎng)絡(luò),原出處不明):

采用線程池和任務(wù)隊列可以實現(xiàn)一種叫做偽異步的 I/O 通信框架,它的模型圖如上圖所示。當有新的客戶端接入時,將客戶端的 Socket 封裝成一個Task(該任務(wù)實現(xiàn)java.lang.Runnable接口)投遞到后端的線程池中進行處理,JDK 的線程池維護一個消息隊列和 N 個活躍線程,對消息隊列中的任務(wù)進行處理。由于線程池可以設(shè)置消息隊列的大小和最大線程數(shù),因此,它的資源占用是可控的,無論多少個客戶端并發(fā)訪問,都不會導(dǎo)致資源的耗盡和宕機。
偽異步I/O通信框架采用了線程池實現(xiàn),因此避免了為每個請求都創(chuàng)建一個獨立線程造成的線程資源耗盡問題。不過因為它的底層任然是同步阻塞的BIO模型,因此無法從根本上解決問題。
1.3 代碼示例
下面代碼中演示了BIO通信(一請求一應(yīng)答)模型。我們會在客戶端創(chuàng)建多個線程依次連接服務(wù)端并向其發(fā)送"當前時間+:hello world",服務(wù)端會為每個客戶端線程創(chuàng)建一個線程來處理。代碼示例出自閃電俠的博客,原地址如下:客戶端
*? *@author閃電俠 *@date2018年10月14日 *@Description:客戶端 */publicclassIOClient{publicstaticvoidmain(String[] args){// TODO 創(chuàng)建多個線程,模擬多個客戶端連接服務(wù)端newThread(() -> {try{? ? ? ? ? ? ? ? Socket socket =newSocket("127.0.0.1",3333);while(true) {try{? ? ? ? ? ? ? ? ? ? ? ? socket.getOutputStream().write((newDate() +": hello world").getBytes());? ? ? ? ? ? ? ? ? ? ? ? Thread.sleep(2000);? ? ? ? ? ? ? ? ? ? }catch(Exception e) {? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }catch(IOException e) {? ? ? ? ? ? }? ? ? ? }).start();? ? }}復(fù)制代碼
服務(wù)端
*@author閃電俠 *@date2018年10月14日 *@Description: 服務(wù)端 */publicclassIOServer{publicstaticvoidmain(String[] args)throwsIOException{// TODO 服務(wù)端處理客戶端連接請求ServerSocket serverSocket =newServerSocket(3333);// 接收到客戶端連接請求之后為每個客戶端創(chuàng)建一個新的線程進行鏈路處理newThread(() -> {while(true) {try{// 阻塞方法獲取新的連接Socket socket = serverSocket.accept();// 每一個新的連接都創(chuàng)建一個線程,負責讀取數(shù)據(jù)newThread(() -> {try{intlen;byte[] data =newbyte[1024];? ? ? ? ? ? ? ? ? ? ? ? ? ? InputStream inputStream = socket.getInputStream();// 按字節(jié)流方式讀取數(shù)據(jù)while((len = inputStream.read(data)) != -1) {? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println(newString(data,0, len));? ? ? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? ? ? }catch(IOException e) {? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }).start();? ? ? ? ? ? ? ? }catch(IOException e) {? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }).start();? ? }}復(fù)制代碼
1.4 總結(jié)
在活動連接數(shù)不是特別高(小于單機1000)的情況下,這種模型是比較不錯的,可以讓每一個連接專注于自己的 I/O 并且編程模型簡單,也不用過多考慮系統(tǒng)的過載、限流等問題。線程池本身就是一個天然的漏斗,可以緩沖一些系統(tǒng)處理不了的連接或請求。但是,當面對十萬甚至百萬級連接的時候,傳統(tǒng)的 BIO 模型是無能為力的。因此,我們需要一種更高效的 I/O 處理模型來應(yīng)對更高的并發(fā)量。
2. NIO (New I/O)
2.1 NIO 簡介
NIO是一種同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,對應(yīng) java.nio 包,提供了 Channel , Selector,Buffer等抽象。
NIO中的N可以理解為Non-blocking,不單純是New。它支持面向緩沖的,基于通道的I/O操作方法。 NIO提供了與傳統(tǒng)BIO模型中的Socket?和ServerSocket?相對應(yīng)的SocketChannel?和ServerSocketChannel?兩種不同的套接字通道實現(xiàn),兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統(tǒng)中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。對于低負載、低并發(fā)的應(yīng)用程序,可以使用同步阻塞I/O來提升開發(fā)速率和更好的維護性;對于高負載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用,應(yīng)使用 NIO 的非阻塞模式來開發(fā)。
2.2 NIO的特性/NIO與IO區(qū)別
如果是在面試中回答這個問題,我覺得首先肯定要從 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 說起。然后,可以從 NIO 的3個核心組件/特性為 NIO 帶來的一些改進來分析。如果,你把這些都回答上了我覺得你對于 NIO 就有了更為深入一點的認識,面試官問到你這個問題,你也能很輕松的回答上來了。
1)Non-blocking IO(非阻塞IO)
IO流是阻塞的,NIO流是不阻塞的。
Java NIO使我們可以進行非阻塞IO操作。比如說,單線程中從通道讀取數(shù)據(jù)到buffer,同時可以繼續(xù)做別的事情,當數(shù)據(jù)讀取到buffer中后,線程再繼續(xù)處理數(shù)據(jù)。寫數(shù)據(jù)也是一樣的。另外,非阻塞寫也是如此。一個線程請求寫入一些數(shù)據(jù)到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。
Java IO的各種流是阻塞的。這意味著,當一個線程調(diào)用read()?或write()?時,該線程被阻塞,直到有一些數(shù)據(jù)被讀取,或數(shù)據(jù)完全寫入。該線程在此期間不能再干任何事情了
2)Buffer(緩沖區(qū))
IO 面向流(Stream oriented),而 NIO 面向緩沖區(qū)(Buffer oriented)。
Buffer是一個對象,它包含一些要寫入或者要讀出的數(shù)據(jù)。在NIO類庫中加入Buffer對象,體現(xiàn)了新庫與原I/O的一個重要區(qū)別。在面向流的I/O中·可以將數(shù)據(jù)直接寫入或者將數(shù)據(jù)直接讀到 Stream 對象中。雖然 Stream 中也有 Buffer 開頭的擴展類,但只是流的包裝類,還是從流讀到緩沖區(qū),而 NIO 卻是直接讀到 Buffer 中進行操作。
在NIO厙中,所有數(shù)據(jù)都是用緩沖區(qū)處理的。在讀取數(shù)據(jù)時,它是直接讀到緩沖區(qū)中的; 在寫入數(shù)據(jù)時,寫入到緩沖區(qū)中。任何時候訪問NIO中的數(shù)據(jù),都是通過緩沖區(qū)進行操作。
最常用的緩沖區(qū)是 ByteBuffer,一個 ByteBuffer 提供了一組功能用于操作 byte 數(shù)組。除了ByteBuffer,還有其他的一些緩沖區(qū),事實上,每一種Java基本類型(除了Boolean類型)都對應(yīng)有一種緩沖區(qū)。
3)Channel (通道)
NIO 通過Channel(通道) 進行讀寫。
通道是雙向的,可讀也可寫,而流的讀寫是單向的。無論讀寫,通道只能和Buffer交互。因為 Buffer,通道可以異步地讀寫。
4)Selectors(選擇器)
NIO有選擇器,而IO沒有。
選擇器用于使用單個線程處理多個通道。因此,它需要較少的線程來處理這些通道。線程之間的切換對于操作系統(tǒng)來說是昂貴的。 因此,為了提高系統(tǒng)效率選擇器是有用的。

2.3 NIO 讀數(shù)據(jù)和寫數(shù)據(jù)方式
通常來說NIO中的所有IO都是從 Channel(通道) 開始的。
從通道進行數(shù)據(jù)讀取 :創(chuàng)建一個緩沖區(qū),然后請求通道讀取數(shù)據(jù)。
從通道進行數(shù)據(jù)寫入 :創(chuàng)建一個緩沖區(qū),填充數(shù)據(jù),并要求通道寫入數(shù)據(jù)。
數(shù)據(jù)讀取和寫入操作圖示:

2.4 NIO核心組件簡單介紹
NIO 包含下面幾個核心的組件:
Channel(通道)
Buffer(緩沖區(qū))
Selector(選擇器)
整個NIO體系包含的類遠遠不止這三個,只能說這三個是NIO體系的“核心API”。我們上面已經(jīng)對這三個概念進行了基本的闡述,這里就不多做解釋了。
2.5 代碼示例
代碼示例出自閃電俠的博客,原地址如下:
客戶端 IOClient.java 的代碼不變,我們對服務(wù)端使用 NIO 進行改造。以下代碼較多而且邏輯比較復(fù)雜,大家看看就好。
*? *@author閃電俠 *@date2019年2月21日 *@Description: NIO 改造后的服務(wù)端 */publicclassNIOServer{publicstaticvoidmain(String[] args) throws IOException {// 1. serverSelector負責輪詢是否有新的連接,服務(wù)端監(jiān)測到新的連接之后,不再創(chuàng)建一個新的線程,// 而是直接將新連接綁定到clientSelector上,這樣就不用 IO 模型中 1w 個 while 循環(huán)在死等Selector serverSelector = Selector.open();// 2. clientSelector負責輪詢連接是否有數(shù)據(jù)可讀Selector clientSelector = Selector.open();newThread(() -> {try{// 對應(yīng)IO編程中服務(wù)端啟動ServerSocketChannel listenerChannel = ServerSocketChannel.open();listenerChannel.socket().bind(newInetSocketAddress(3333));listenerChannel.configureBlocking(false);listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);while(true) {// 監(jiān)測是否有新的連接,這里的1指的是阻塞的時間為 1msif(serverSelector.select(1) >0) {Setset= serverSelector.selectedKeys();Iterator keyIterator =set.iterator();while(keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if(key.isAcceptable()) {try{// (1)// 每來一個新連接,不需要創(chuàng)建一個線程,而是直接注冊到clientSelectorSocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();clientChannel.configureBlocking(false);clientChannel.register(clientSelector, SelectionKey.OP_READ);}finally{keyIterator.remove();}}}}}}catch(IOException ignored) {}}).start();newThread(() -> {try{while(true) {// (2) 批量輪詢是否有哪些連接有數(shù)據(jù)可讀,這里的1指的是阻塞的時間為 1msif(clientSelector.select(1) >0) {Setset= clientSelector.selectedKeys();Iterator keyIterator =set.iterator();while(keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if(key.isReadable()) {try{SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// (3) 面向 BufferclientChannel.read(byteBuffer);byteBuffer.flip();System.out.println(? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());}finally{keyIterator.remove();key.interestOps(SelectionKey.OP_READ);}}}}}}catch(IOException ignored) {}}).start();}}復(fù)制代碼
為什么大家都不愿意用 JDK 原生 NIO 進行開發(fā)呢?從上面的代碼中大家都可以看出來,是真的難用!除了編程復(fù)雜、編程模型難之外,它還有以下讓人詬病的問題:
JDK 的 NIO 底層由 epoll 實現(xiàn),該實現(xiàn)飽受詬病的空輪詢 bug 會導(dǎo)致 cpu 飆升 100%
項目龐大之后,自行實現(xiàn)的 NIO 很容易出現(xiàn)各類 bug,維護成本較高,上面這一坨代碼我都不能保證沒有 bug
Netty 的出現(xiàn)很大程度上改善了 JDK 原生 NIO 所存在的一些讓人難以忍受的問題。
3. AIO (Asynchronous I/O)
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改進版 NIO 2,它是異步非阻塞的IO模型。異步 IO 是基于事件和回調(diào)機制實現(xiàn)的,也就是應(yīng)用操作之后會直接返回,不會堵塞在那里,當后臺處理完成,操作系統(tǒng)會通知相應(yīng)的線程進行后續(xù)的操作。
AIO 是異步IO的縮寫,雖然 NIO 在網(wǎng)絡(luò)操作中,提供了非阻塞的方法,但是 NIO 的 IO 行為還是同步的。對于 NIO 來說,我們的業(yè)務(wù)線程是在 IO 操作準備好時,得到通知,接著就由這個線程自行進行 IO 操作,IO操作本身是同步的。
需要java學(xué)習(xí)路線圖的關(guān)注gzh“程序員小x”私信領(lǐng)取哦!另外喜歡這篇文章的可以給筆者點個贊,關(guān)注一下,每天都會分享Java相關(guān)文章!還有不定時的福利贈送,包括整理的學(xué)習(xí)資料,面試題,源碼等~~