1. 程式人生 > >對於BIO/NIO/AIO,你還只停留在燒開水的水平嗎?

對於BIO/NIO/AIO,你還只停留在燒開水的水平嗎?

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){}不斷檢測多路複用器上所有連線的狀態,這些狀態通過呼叫 SelectionKey.isAcceptable()、SelectionKey.isReadable() 等方法讀取。發現請求有效,就開啟一個執行緒進行處理,無效的請求,就不需要建立執行緒進行處理。

與BIO對比不難發現,這種方式相比BIO一次連線建立一個執行緒大大減少了執行緒的建立數量,效能豈能不提高。

6.AIO:非同步非阻塞的程式設計方式

BIO/NIO都需要在呼叫讀寫方法後,要麼一直等待,要麼輪詢檢視,直到有了結果再來執行後續程式碼,這就是同步操作了。

而AIO則是真正的非同步,當進行讀寫操作時,只須直接呼叫API的 read 或 write 方法即可。對於讀操作而言,當有流可讀取時,作業系統會將可讀的流傳入 read 方法的緩衝區,並通知應用程式;對於寫操作而言,當作業系統將 write 方法傳遞的流寫入完畢時,作業系統主動通知應用程式。你可以理解為,read/write 方法都是非同步的,完成後會主動呼叫回撥函式,這也就是同步與非同步真正的區別了。

示例程式碼:

public static void main(String[] args) {
   AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();
   assc.bind(new InetSocketAddress("localhost", 8080));
   //非阻塞方法,註冊回撥函式,只能接受一個連線
   assc.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {

     @Override
     public void completed(AsynchronousSocketChannel asc, Object attachment) {
     }

     @Override
     public void failed(Throwable exc, Object attachment) {  
     }
   });
}

(注:Java7後引入AIO ,但不同作業系統底層原理不一致,比如Linux的epoll, Window的iocp)

7.後續

文章講到這裡,其實只是開始。

如今,大名鼎鼎的IO多路複用你已經知道了What,但我們依舊有著許多的Why不理解,Selector為什麼可以做到多路複用?selector.select() 方法的呼叫經歷了什麼?作業系統又在其中扮演著什麼樣的角色?AIO中作業系統是如何做到主動通知應用程式呼叫回撥函式?...

對於這些問題,你是否喪失了深究下去的興趣?

【未完待續