詳解 Java 中 4 種 IO 模型

來源:ncoding
ncoding.com/2018/04/02/java/io.html

同步、異步、阻塞、非阻塞都是和I/O(輸入輸出)有關的概念,最簡單的文件讀取就是I/O操作。而在文件讀取這件事兒上,可以有多種方式。

本篇會先介紹一下I/O的基本概念,通過一個生活例子來分別解釋下這幾種I/O模型,以及Java支持的I/O模型。

基本概念

在解釋I/O模型之前,我先說明一下幾個操作系統的概念

文件描述符fd

文件描述符(file descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每一個進程所維護的該進程打開文件的記錄表。 當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。 在程序設計中,一些涉及底層的程序編寫往往會圍繞着文件描述符展開。文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。

緩存I/O

緩存I/O又被稱作標準I/O,大多數文件系統的默認I/O操作都是緩存I/O。在Linux的緩存I/O機制中, 操作系統會將I/O的數據緩存在文件系統的頁緩存中,也就是說,數據會先被拷貝到操作系統內核的緩衝區中, 然後纔會從操作系統內核的緩衝區拷貝到應用程序的地址空間。

緩存I/O的缺點是數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的CPU以及內存開銷是非常大的。

下面我以一個生活中燒開水的例子來形象解釋一下同步、異步、阻塞、非阻塞概念。

同步和異步

說到燒水,我們都是通過熱水壺來燒水的。在很久之前,科技還沒有這麼發達的時候,如果我們要燒水, 需要把水壺放到火爐上,我們通過觀察水壺內的水的沸騰程度來判斷水有沒有燒開。

隨着科技的發展,現在市面上的水壺都有了提醒功能,當我們把水壺插電之後,水壺水燒開之後會通過聲音提醒我們水開了。

對於燒水這件事兒來說,傳統水壺的燒水就是同步的,高科技水壺的燒水就是異步的。

同步請求

A調用B,B的處理是同步的,在處理完之前他不會通知A,只有處理完之後纔會明確的通知A。

異步請求

A調用B,B的處理是異步的,B在接到請求後先告訴A我已經接到請求了,然後異步去處理,處理完之後通過回調等方式再通知A。

所以說,同步和異步最大的區別就是被調用方的執行方式和返回時機。 同步指的是被調用方做完事情之後再返回,異步指的是被調用方先返回,然後再做事情,做完之後再想辦法通知調用方。

阻塞和非阻塞

還是那個燒水的例子,當你把水放到水壺裏面,按下開關後,你可以坐在水壺前面,別的事情什麼都不做, 一直等着水燒好。你還可以先去客廳看電視,等着水開就好了。

對於你來說,坐在水壺前面等就是阻塞的,去客廳看電視等着水開就是非阻塞的。

阻塞請求

A調用B,A一直等着B的返回,別的事情什麼也不幹。

非阻塞請求

A調用B,A不用一直等着B的返回,先去忙別的事情了。

所以說,阻塞和非阻塞最大的區別就是在被調用方返回結果之前的這段時間內,調用方是否一直等待。 阻塞指的是調用方一直等待別的事情什麼都不做。非阻塞指的是調用方先去忙別的事情。

阻塞、非阻塞和同步、異步的區別

首先,前面已經提到過,阻塞、非阻塞和同步、異步其實針對的對象是不一樣的。

給我大聲念三遍下面的句子

阻塞、非阻塞說的是調用者。同步、異步說的是被調用者。

阻塞、非阻塞說的是調用者。同步、異步說的是被調用者。

阻塞、非阻塞說的是調用者。同步、異步說的是被調用者。

有人認爲阻塞和同步是一回事兒,非阻塞和異步是一回事。但是這是不對的。

同步包含阻塞和非阻塞

我們是用傳統的水壺燒水。在水燒開之前我們一直做在水壺前面,等着水開。這就是阻塞的。

我們是用傳統的水壺燒水。在水燒開之前我們先去客廳看電視了,但是水壺不會主動通知我們, 需要我們時不時的去廚房看一下水有沒有燒開,這就是非阻塞的。

異步包含阻塞和非阻塞

我們是用帶有提醒功能的水壺燒水。在水燒發出提醒之前我們一直做在水壺前面,等着水開。這就是阻塞的。

我們是用帶有提醒功能的水壺燒水。在水燒發出提醒之前我們先去客廳看電視了,等水壺發出聲音提醒我們。這就是非阻塞的。推薦閱讀:46 道阿里巴巴 Java 面試題,你會幾道?

Unix中的五種I/O模型

對於一次I/O訪問(以read舉例),數據會先被拷貝到操作系統內核的緩衝區中,然後纔會從操作系統內核的緩衝區拷貝到應用程序的地址空間。 所以說,當一個read操作發生時,它會經歷兩個階段:

第一階段:等待數據準備 (Waiting for the data to be ready)。

第二階段:將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)。

對於socket流而言

第一階段:通常涉及等待網絡上的數據分組到達,也就是被複制到內核的某個緩衝區。

第二階段:把數據從內核緩衝區複製到應用進程緩衝區。

Unix下五種I/O模型:

  1. 同步阻塞I/O

  2. 同步非阻塞I/O

  3. I/O多路複用(select和poll)

  4. 信號驅動I/O(SIGIO)

  5. 異步非阻塞 IO

同步阻塞I/O

阻塞I/O下請求無法立即完成則保持阻塞,阻塞I/O分爲如下兩個階段。

階段1:等待數據就緒。網絡I/O的情況就是等待遠端數據陸續抵達,也就是網絡數據被複制到內核緩存區中,磁盤I/O的情況就是等待磁盤數據從磁盤上讀取到內核態內存中。

階段2:數據拷貝。出於系統安全,用戶態的程序沒有權限直接讀取內核態內存,因此內核負責把內核態內存中的數據拷貝一份到用戶態內存中。

這兩個階段必須都完成後才能繼續下一步操作

所以,blocking IO的特點就是在IO執行的兩個階段都被block了。

同步非阻塞I/O

就是階段1的時候用戶進程可選擇做其他事情,通過輪詢的方式看看內核緩衝區是否就緒。如果數據就緒,再去執行階段2。

也就是說非阻塞的recvform系統調用調用之後,進程並沒有被阻塞,內核馬上返回給進程,如果數據還沒準備好, 此時會返回一個error。進程在返回之後,可以乾點別的事情,然後再發起recvform系統調用。

重複上面的過程, 循環往復的進行recvform系統調用。這個過程通常被稱之爲輪詢。輪詢檢查內核數據,直到數據準備好, 再拷貝數據到進程,進行數據處理。需要注意,第2階段的拷貝數據整個過程,進程仍然是屬於阻塞的狀態。推薦閱讀:Java 8 開發的 4 大頂級技巧

在linux下,可以通過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操作時,流程如圖所示:

所以,nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數據好了沒有。

I/O多路複用

我這裏只想重點解釋一下I/O多路複用這種模型,因爲現在用的最多。很多地方也稱爲事件驅動IO模型,只是叫法不同,意思都一個樣。

IO多路複用是指內核一旦發現進程指定的一個或者多個IO條件準備讀取,它就通知該進程。

目前支持I/O多路複用的系統調用有 select、pselect、poll、epoll,I/O多路複用就是通過一種機制,一個進程可以監視多個描述符, 一旦某個文件描述符fd就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。

但select、pselect、poll、epoll本質上都是同步I/O,因爲他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的, 而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。

相比較於同步非阻塞I/O,它的改進的地方在於,原來需要用戶進程去輪詢的這事兒交給了內核線程幫你完成, 而且這個內核線程可以等待多個socket,能實現同時對多個IO端口進行監聽。

多路複用的特點是通過一種機制一個進程能同時等待IO文件描述符,內核監視這些文件描述符(套接字描述符), 其中的任意一個進入讀就緒狀態,select, poll,epoll函數就可以返回。對於監視的方式, 又可以分爲 select, poll, epoll三種方式。

所以,如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用多線程 + 阻塞IO的web server性能更好,可能延遲還更大。 也就是說,select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。推薦閱讀:Spring Boot 的 10 個核心模塊

高併發的程序一般使用同步非阻塞方式而非多線程 + 同步阻塞方式。要理解這一點,首先要扯到併發和並行的區別。 比如去某部門辦事需要依次去幾個窗口,辦事大廳裏的人數就是併發數,而窗口個數就是並行度。 也就是說併發數是指同時進行的任務數(如同時服務的 HTTP 請求),而並行數是可以同時工作的物理資源數量(如 CPU 核數)。

通過合理調度任務的不同階段,併發數可以遠遠大於並行度,這就是區區幾個 CPU 可以支持上萬個用戶併發請求的奧祕。 在這種高併發的情況下,爲每個任務(用戶請求)創建一個進程或線程的開銷非常大。而同步非阻塞方式可以把多個 IO 請求丟到後臺去, 這就可以在一個進程裏服務大量的併發 IO 請求。

IO多路複用歸爲同步阻塞模式

異步非阻塞 IO

相對於同步IO,異步IO不是順序執行。用戶進程進行aio_read系統調用之後,無論內核數據是否準備好,都會直接返回給用戶進程, 然後用戶態進程可以去做別的事情。等到socket數據準備好了,內核直接複製數據給進程,然後從內核向進程發送通知。IO兩個階段, 進程都是非阻塞的。

Linux提供了AIO庫函數實現異步,但是用的很少。目前有很多開源的異步IO庫,例如libevent、libev、libuv。異步過程如下圖所示:

更詳細的分析可參考 聊聊Linux5種IO模型

Java中四種I/O模型

上一章所述Unix中的五種I/O模型,除信號驅動I/O外,Java對其它四種I/O模型都有所支持。

  1. Java傳統IO模型即是同步阻塞I/O

  2. NIO是同步非阻塞I/O

  3. 通過NIO實現的Reactor模式即是I/O多路複用模型的實現

  4. 通過AIO實現的Proactor模式即是異步I/O模型的實現

關注公衆號Java技術棧回覆"面試"獲取我整理的2020最全面試題及答案。

推薦去我的博客閱讀更多:

1.Java JVM、集合、多線程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、後端、架構、阿里巴巴等大廠最新面試題

覺得不錯,別忘了點贊+轉發哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章