相信你們在網上看過很多講解 BIO/NIO/AIO 的文章,文章中舉起栗子來更是夯吃夯吃一大堆,我是越看越以爲 What are you 你講啥嘞?java
本文將針對 BIO/NIO/AIO 、阻塞與非阻塞、同步與異步等特別容易混淆的概念進行對比區分,理清混亂的思路。編程
數據的讀取寫入必須阻塞在一個線程內等待其完成。網絡
這裏使用那個經典的燒開水例子,這裏假設一個燒開水的場景,有一排水壺在燒開水,BIO的工做模式就是, 叫一個線程停留在一個水壺那,直到這個水壺燒開,纔去處理下一個水壺。可是實際上線程在等待水壺燒開的時間段什麼都沒有作。異步
同時支持阻塞與非阻塞模式,但這裏咱們以其同步非阻塞I/O模式來講明,那麼什麼叫作同步非阻塞?若是還拿燒開水來講,NIO的作法是叫一個線程不斷的輪詢每一個水壺的狀態,看看是否有水壺的狀態發生了改變,從而進行下一步的操做。socket
異步非阻塞與同步非阻塞的區別在哪裏?異步非阻塞無需一個線程去輪詢全部IO操做的狀態改變,在相應的狀態改變後,系統會通知對應的線程來處理。對應到燒開水中就是,爲每一個水壺上面裝了一個開關,水燒開以後,水壺會自動通知我水燒開了。ide
上面這些燒開水(或者服務員端菜)的例子百度一下至關多,但只能幫你理解些相關概念,使你知其然但不知其因此然,下面我會對概念進一步加深理解,並加以區分。函數
同步和異步是針對應用程序和內核的交互而言的,同步指的是用戶進程觸發IO操做並等待或者輪詢的去查看IO操做是否就緒,而異步是指用戶進程觸發IO操做之後便開始作本身的事情,而當IO操做已經完成的時候會獲得IO完成的通知。性能
簡而言之,同步和異步最關鍵的區別在於同步必須等待(BIO)或者主動的去詢問(NIO)IO是否完成,而異步(AIO)操做提交後只需等待操做系統的通知便可。(思考一下:操做系統底層經過什麼去通知數據使用者?)網站
大型網站通常都會使用消息中間件進行解藕、異步、削峯,生產者將消息發送給消息中間件就返回,消息中間件將消息轉發到消費者進行消費,這種操做方式其實就是異步。操作系統
與之相比,什麼是同步?
生產者將消息發送到消息中間件,消息中間件將消息發送給消費者,消息者消費後返回響應給消息中間件,消息中間件返回響應給生產者,該過程由始至終都須要生產者進行參與,這就是同步操做。
(注:上面的舉例只用於理解BIO/NIO概念,不表明消息中間件的真實使用過程)
阻塞和非阻塞是針對於進程在訪問數據的時候,根據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()
方法會一直阻塞到某個註冊的通道有事件就緒,而後返回當前就緒的通道數,也就是非阻塞概念中提到的狀態值。
咱們都據說過NIO具備IO多路複用,其實關鍵點就在於NIO建立一個鏈接後,是不須要建立對應的一個線程,這個鏈接會被註冊到多路複用器(Selector)上面,因此全部的鏈接只須要一個線程就能夠進行管理,當這個線程中的多路複用器進行輪詢的時候,發現鏈接上有請求數據的話,纔開啓一個線程進行處理,也就是一個有效請求一個線程模式。若是鏈接沒有數據,是沒有工做線程來處理的。
光講概念恐怕讀者很難聽的懂,因此我仍是以上面那張圖中的代碼講解。
在代碼中,main方法所在的主線程擁有多路複用器並開啓了一個主機端口進行通訊,全部的客戶端鏈接都會被註冊到主線程所在的多路複用器,經過輪詢while(true){}
不斷檢測多路複用器上全部鏈接的狀態,這些狀態經過調用 SelectionKey.isAcceptable()、SelectionKey.isReadable() 等方法讀取。發現請求有效,就開啓一個線程進行處理,無效的請求,就不須要建立線程進行處理。
與BIO對比不難發現,這種方式相比BIO一次鏈接建立一個線程大大減小了線程的建立數量,性能豈能不提升。
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)
文章講到這裏,其實只是開始。
現在,大名鼎鼎的IO多路複用你已經知道了What,但咱們依舊有着許多的Why不理解,Selector爲何能夠作到多路複用?selector.select() 方法的調用經歷了什麼?操做系統又在其中扮演着什麼樣的角色?AIO中操做系統是如何作到主動通知應用程序調用回調函數?...
對於這些問題,你是否喪失了深究下去的興趣?
【未完待續】