1.前言
相信大家在網上看過不少講解 BIO/NIO/AIO 的文章,文章中舉起栗子來更是夯喫夯喫一大堆,我是越看越覺得 What are you 你講啥嘞?
本文將針對 BIO/NIO/AIO 、阻塞與非阻塞、同步與異步等特別容易混淆的概念進行對比區分,理清混亂的思路。
2.魔幻的IO模型
BIO (同步阻塞I/O)
數據的讀取寫入必須阻塞在一個線程內等待其完成。
這裏使用那個經典的燒開水例子,這裏假設一個燒開水的場景,有一排水壺在燒開水,BIO的工作模式就是, 叫一個線程停留在一個水壺那,直到這個水壺燒開,纔去處理下一個水壺。但是實際上線程在等待水壺燒開的時間段什麼都沒有做。
NIO(同步非阻塞)
同時支持阻塞與非阻塞模式,但這裏我們以其同步非阻塞I/O模式來說明,那麼什麼叫做同步非阻塞?如果還拿燒開水來說,NIO的做法是叫一個線程不斷的輪詢每個水壺的狀態,看看是否有水壺的狀態發生了改變,從而進行下一步的操作。
AIO (異步非阻塞I/O)
異步非阻塞與同步非阻塞的區別在哪裏?異步非阻塞無需一個線程去輪詢所有IO操作的狀態改變,在相應的狀態改變後,系統會通知對應的線程來處理。對應到燒開水中就是,爲每個水壺上面裝了一個開關,水燒開之後,水壺會自動通知我水燒開了。
上面這些燒開水(或者服務員端菜)的例子百度一下相當多,但只能幫你理解些相關概念,使你知其然但不知其所以然,下面我會對概念進一步加深理解,並加以區分。
3.同步與異步的區別
同步和異步是針對應用程序和內核的交互而言的,同步指的是用戶進程觸發IO操作並等待或者輪詢的去查看IO操作是否就緒,而異步是指用戶進程觸發IO操作以後便開始做自己的事情,而當IO操作已經完成的時候會得到IO完成的通知。
簡而言之,同步和異步最關鍵的區別在於同步必須等待(BIO)或者主動的去詢問(NIO)IO是否完成,而異步(AIO)操作提交後只需等待操作系統的通知即可。(思考一下:操作系統底層通過什麼去通知數據使用者?)
大型網站一般都會使用消息中間件進行解藕、異步、削峯,生產者將消息發送給消息中間件就返回,消息中間件將消息轉發到消費者進行消費,這種操作方式其實就是異步。
與之相比,什麼是同步?
生產者將消息發送到消息中間件,消息中間件將消息發送給消費者,消息者消費後返回響應給消息中間件,消息中間件返回響應給生產者,該過程由始至終都需要生產者進行參與,這就是同步操作。
(注:上面的舉例只用於理解BIO/NIO概念,不代表消息中間件的真實使用過程)
4.阻塞和非阻塞的區別
阻塞和非阻塞是針對於進程在訪問數據的時候,根據IO操作的就緒狀態來採取的不同方式,說白了是一種讀取或者寫入操作方法的實現方式,阻塞方式下讀取或者寫入函數將一直等待(BIO),而非阻塞方式下,讀取或者寫入方法會立即返回一個狀態值(NIO)。
BIO對應的Socket網絡編程代碼如下,其中server.accept()
代碼會一直阻塞當前線程,直到有新的客戶端與之連接後,就創建一個新的線程進行處理,注意這裏是一次連接創建一個線程。
public static void main(String[] args) throws IOException {
int port = 8899;
// 定義一個ServiceSocket監聽在端口8899上
ServerSocket server = new ServerSocket(port);
System.out.println("等待與客戶端建立連接...");
while (true) {
// server嘗試接收其他Socket的連接請求,server的accept方法是阻塞式的
Socket socket = server.accept();
// 每接收到一個Socket就建立一個新的線程來處理它
new Thread(new Task(socket)).start();
}
// server.close();
}
NIO的Socket網絡編程代碼如下圖(在網上找了半天),我們只需要觀察NIO的關鍵兩個點:輪詢、IO多路複用。
找到while(true){}
代碼就找到了輪詢的代碼,其中調用的 selector.select()
方法會一直阻塞到某個註冊的通道有事件就緒,然後返回當前就緒的通道數,也就是非阻塞概念中提到的狀態值。
5.IO多路複用
我們都聽說過NIO具有IO多路複用,其實關鍵點就在於NIO創建一個連接後,是不需要創建對應的一個線程,這個連接會被註冊到多路複用器(Selector)上面,所以所有的連接只需要一個線程就可以進行管理,當這個線程中的多路複用器進行輪詢的時候,發現連接上有請求數據的話,纔開啓一個線程進行處理,也就是一個有效請求一個線程模式。如果連接沒有數據,是沒有工作線程來處理的。
光講概念恐怕讀者很難聽的懂,所以我還是以上面那張圖中的代碼講解。
在代碼中,main方法所在的主線程擁有多路複用器並開啓了一個主機端口進行通信,所有的客戶端連接都會被註冊到主線程所在的多路複用器,通過輪詢while(true){}
不斷檢測多路複用器上所有連接的狀態,也就是 selectedKey 提供的API。發現請求有效,就開啓一個線程進行處理,無效的請求,就不需要創建線程進行處理。
與BIO對比不難發現,這種方式相比BIO一次連接創建一個線程大大減少了線程的創建數量,性能豈能不提高。
6.AIO:異步非阻塞的編程方式
BIO/NIO都需要在調用讀寫方法後,要麼一直等待,要麼輪詢查看,直到有了結果再來執行後續代碼,這就是同步操作了。
而AIO則是真正的異步,當進行讀寫操作時,只須直接調用API的 read 或 write 方法即可。對於讀操作而言,當有流可讀取時,操作系統會將可讀的流傳入 read 方法的緩衝區,並通知應用程序;對於寫操作而言,當操作系統將 write 方法傳遞的流寫入完畢時,操作系統主動通知應用程序。你可以理解爲,read/write 方法都是異步的,完成後會主動調用回調函數,這也就是同步與異步真正的區別了。