阿里、騰訊熱門面試題:聊聊Unix與Java的IO模型?(含詳細解析)
眾所周知
如果去百度、騰訊等一線大廠面試,一定會深入考候選人的基礎技術功底,其中尤為關鍵和重視的就是IO相關的技術和知識。
而要搞明白IO相關的概念,首先就得弄清楚同步與非同步,阻塞與非阻塞到底是什麼意思。
同步與非同步
想要搞明白IO模型,就先得搞明白“同步”與“非同步”的關係。
所謂的 “同步” ,比如說呼叫者去呼叫一個介面,這個介面比如要執行一些磁碟檔案讀寫操作,或者是網路通訊操作。
假設是“同步”的模式,呼叫者必須要等待這個介面的磁碟讀寫或者網路通訊的操作執行完畢了,呼叫者才能返回,這就是“同步”,如下圖所示:

,就是說這個呼叫者呼叫介面之後,直接就返回了,他去幹別的事兒了,也不管那個介面的磁碟讀寫或者是網路通訊是否成功。
然後這個介面後續如果幹完了自己的任務,比如寫完了檔案或者是什麼的,會反過來通知呼叫者,之前你的那個呼叫成功了。可以通過一些內部通訊機制來通知,也可以通過回撥函式來通知,如下圖:

用生活中的例子理解同步與非同步
如果給大家舉個生活中的例子,那麼就可以用買菸這個事兒來舉個例子
比如說現在你要去一個櫃檯買很多條香菸,但是現在櫃檯沒那麼多貨,他需要打電話給庫房來查一下有沒有足夠的貨。
這個時候,庫房的工作人員正好去吃飯了,那現在你有 兩種選擇 :
第一種選擇,你可以在櫃檯等著,一直等待庫房工作人員回來,櫃檯專員打通電話給他查到了庫存是否充足,你再走。
這個就是 “同步” ,你找櫃檯工作人員買香菸,他要打電話給庫房工作人員問庫存,如果你選擇“同步”模式,那麼你就在櫃檯一直等著,直到成功查詢到庫存為止。
第二種選擇,你可以先回家乾點兒別的,比如說洗衣服做飯之類的,然後過了一會兒,櫃檯工作人員打通電話給庫房工作人員,查到香菸庫存了,就會打個電話給你,告訴你這個事兒。
這就是 “非同步” ,你跟櫃檯工作人員說了這個事兒,就直接走了,幹別的去了,櫃檯工作人員後面完成他的任務之後,就會反過來打電話回撥通知你。
阻塞與非阻塞
實際上阻塞與非阻塞的概念,通常是針對底層的IO操作來說的。
比如現在我們的程式想要通過網路讀取資料,如果是阻塞IO模式,一旦發起請求到作業系統核心去從網路中讀取資料,就會 阻塞 在那裡,必須要等待網路中的資料到達了之後,才能從網路讀取資料到核心,再從核心返回給程式,如下圖:

,指的就是程式傳送請求給核心要從網路讀取資料,但是此時網路中的資料還沒到,此時不會阻塞住,核心會返回一個異常訊息給程式。
程式就可以乾點兒別的,然後過一會兒再來發起一次請求給核心,讓核心嘗試從網路讀取資料。
因為如果網路中的資料還沒到位,是不會阻塞住程式的,需要程式自己不斷的輪詢核心去嘗試讀取資料,所以這種IO就是非阻塞的。如下圖:

大家不要把“同步/非同步”概念和“阻塞/非阻塞”概念混淆起來,實際上他們是兩組不同的概念。
“同步/非同步”更多的是針對比如介面呼叫,服務呼叫,API類庫呼叫,類似這樣的場景。
而“阻塞/非阻塞”概念針對的是底層IO操作的場景,比如磁碟IO,網路IO。但是在Java IO模型裡,兩種概念之間是有一定的關聯關係的 。
Unix支援的5種IO模型
Unix作業系統支援的IO模型主要就是5種:
- 阻塞IO :就是上面圖裡的那種阻塞IO模式,程式發起請求之後會阻塞,一直到系統核心發現網路中有資料到達了,拷貝資料給程式程序了,才能返回
- 非阻塞IO :就是上面圖裡的那種非阻塞IO模式,程式發起請求讀取資料,系統核心發現網路資料還沒到,就返回一個異常資訊,程式不會阻塞在IO操作上,但是過一會兒還得再來發起請求給核心,直到核心發現網路資料到達了,此時就會拷貝資料給程式程序
- IO多路複用 :這個下面來講
- 訊號驅動式IO :一般很少用到,這裡不說明
- 非同步IO :下面來講
JDK 1.4之前的同步阻塞IO
在JDK 1.4之前,主要就是同步阻塞IO模型,在Java裡叫做BIO。
在Java程式碼裡呼叫IO相關介面,發起IO操作之後,Java程式就會同步等待,這個同步指的是Java程式呼叫IO API介面的層面而言。
而IO API在底層的IO操作是基於 阻塞IO 來的,向作業系統核心發起IO請求,系統核心會等待資料就位之後,才會執行IO操作,執行完畢了才會返回。
JDK 1.4之後的同步非阻塞NIO
在JDK 1.4之後提供了 NIO ,他的概念是 同步非阻塞 ,也就是說如果你呼叫NIO介面去執行IO操作,其實還是同步等待的,但是在底層的IO操作上 ,會對系統核心發起非阻塞IO請求,以非阻塞的形式來執行IO。
也就是說,如果底層資料沒到位,那麼核心返回異常資訊,不會阻塞住,但是NIO介面內部會採用非阻塞方式過一會兒再次呼叫核心發起IO請求,直到成功為止。
但是之所以說是同步非阻塞,這裡的“同步”指的就是因為在你的Java程式碼呼叫NIO介面層面是同步的,你還是要同步等待底層IO操作真正完成了才可以返回,只不過在執行底層IO的時候採用了非阻塞的方式來執行罷了。
NIO網路通訊與IO多路複用模型
實際上,如果基於NIO進行網路通訊,採取的就是多路複用的IO模型,這個多路複用IO模型針對的是網路通訊中的IO場景來說的。
簡單來說,就是在基於Socket進行網路通訊的時候,如果有多個客戶端跟你的服務端建立了Socket連線,那你就需要維護多個Socket連線。
而所謂的多路複用IO模型,就是說你的Java程式碼直接通過一個select函式呼叫,直接會進入一個同步等待的狀態。
這也是為什麼說NIO一定是“同步”的,因為你必須在這裡同步等待某個Socket連線有請求到來。
接著你就要同步等著select函式去對底層的多個 Socket 連線進行輪詢,不斷的檢視各個 Socket 連線誰有請求到達,就可以讓select函式返回,交給我們的Java程式來處理。
select函式在底層會通過非阻塞的方式輪詢各個Socket,任何一個Socket如果沒有資料到達,那麼非阻塞的特性會立即返回一個資訊。
然後select函式可以輪詢下一個Socket,不會阻塞在某個Socket上,所以底層是基於這種非阻塞的模式來“監視”各個Socket誰有資料到達的。
這就是所謂的 “同步非阻塞” ,但是因為作業系統把上述工作都封裝在一個select函式呼叫裡了,可以對多路Socket連線同時進行監視,所以就把這種模型稱之為 “IO多路複用”模型 。
通過這種IO多路複用的模型,就可以用一個執行緒,呼叫一個select函式,然後監視大量的客戶端連線了,如下圖:

AIO以及非同步IO模型
最後就是JDK 1.7之後,又支援了 AIO ,也叫做NIO 2.0,他就支援 非同步IO模型 了。
我們先說一下非同步IO模型是什麼意思。
簡單來說,就是你的Java程式可以基於AIO API發起一個請求,比如說接收網路資料,AIO API底層會基於非同步IO模型來呼叫作業系統核心。
此時不需要去管這個IO是否成功了,AIO介面會直接返回,你的Java程式也會直接返回。
然後,你的Java程式就可以去幹別的事兒了。大家聯想一下上面說的那個非同步的例子,就可以理解這裡為什麼叫做非同步了。
因為BIO、NIO都是同步的,你發起IO請求,都必須同步等待IO操作完成。
但是這裡你發起一個IO請求,直接AIO介面就返回了,你就可以幹別的事兒了,純非同步的方式。
不過你需要提供一個回撥函式給AIO介面,一旦底層系統核心完成了具體的IO請求,比如網路讀寫之類的,就會回撥你提供的回撥函式。
比如說你要是通過網路讀取資料,那麼此時AIO介面就會把作業系統非同步讀取到的資料交給你的回撥函式。
整個過程如下圖:
