详解 Java 中 4 种 I/O 模型
同步、異步、阻塞、非阻塞都是和I/O(輸入輸出)有關(guān)的概念,最簡單的文件讀取就是I/O操作。而在文件讀取這件事兒上,可以有多種方式。
本篇會(huì)先介紹一下I/O的基本概念,通過一個(gè)生活例子來分別解釋下這幾種I/O模型,以及Java支持的I/O模型。
基本概念
在解釋I/O模型之前,我先說明一下幾個(gè)操作系統(tǒng)的概念
文件描述符fd
文件描述符(file descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語,是一個(gè)用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個(gè)非負(fù)整數(shù)。實(shí)際上,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。 當(dāng)程序打開一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。 在程序設(shè)計(jì)中,一些涉及底層的程序編寫往往會(huì)圍繞著文件描述符展開。文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。
緩存I/O
緩存I/O又被稱作標(biāo)準(zhǔn)I/O,大多數(shù)文件系統(tǒng)的默認(rèn)I/O操作都是緩存I/O。在Linux的緩存I/O機(jī)制中, 操作系統(tǒng)會(huì)將I/O的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存中,也就是說,數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中, 然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。
緩存I/O的缺點(diǎn)是數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的CPU以及內(nèi)存開銷是非常大的。
下面我以一個(gè)生活中燒開水的例子來形象解釋一下同步、異步、阻塞、非阻塞概念。
同步和異步
說到燒水,我們都是通過熱水壺來燒水的。在很久之前,科技還沒有這么發(fā)達(dá)的時(shí)候,如果我們要燒水, 需要把水壺放到火爐上,我們通過觀察水壺內(nèi)的水的沸騰程度來判斷水有沒有燒開。
隨著科技的發(fā)展,現(xiàn)在市面上的水壺都有了提醒功能,當(dāng)我們把水壺插電之后,水壺水燒開之后會(huì)通過聲音提醒我們水開了。
對于燒水這件事兒來說,傳統(tǒng)水壺的燒水就是同步的,高科技水壺的燒水就是異步的。
同步請求
A調(diào)用B,B的處理是同步的,在處理完之前他不會(huì)通知A,只有處理完之后才會(huì)明確的通知A。
異步請求
A調(diào)用B,B的處理是異步的,B在接到請求后先告訴A我已經(jīng)接到請求了,然后異步去處理,處理完之后通過回調(diào)等方式再通知A。
所以說,同步和異步最大的區(qū)別就是被調(diào)用方的執(zhí)行方式和返回時(shí)機(jī)。 同步指的是被調(diào)用方做完事情之后再返回,異步指的是被調(diào)用方先返回,然后再做事情,做完之后再想辦法通知調(diào)用方。
阻塞和非阻塞
還是那個(gè)燒水的例子,當(dāng)你把水放到水壺里面,按下開關(guān)后,你可以坐在水壺前面,別的事情什么都不做, 一直等著水燒好。你還可以先去客廳看電視,等著水開就好了。
對于你來說,坐在水壺前面等就是阻塞的,去客廳看電視等著水開就是非阻塞的。
阻塞請求
A調(diào)用B,A一直等著B的返回,別的事情什么也不干。
非阻塞請求
A調(diào)用B,A不用一直等著B的返回,先去忙別的事情了。
所以說,阻塞和非阻塞最大的區(qū)別就是在被調(diào)用方返回結(jié)果之前的這段時(shí)間內(nèi),調(diào)用方是否一直等待。 阻塞指的是調(diào)用方一直等待別的事情什么都不做。非阻塞指的是調(diào)用方先去忙別的事情。
阻塞、非阻塞和同步、異步的區(qū)別
首先,前面已經(jīng)提到過,阻塞、非阻塞和同步、異步其實(shí)針對的對象是不一樣的。
給我大聲念三遍下面的句子
阻塞、非阻塞說的是調(diào)用者。同步、異步說的是被調(diào)用者。
阻塞、非阻塞說的是調(diào)用者。同步、異步說的是被調(diào)用者。
阻塞、非阻塞說的是調(diào)用者。同步、異步說的是被調(diào)用者。
有人認(rèn)為阻塞和同步是一回事兒,非阻塞和異步是一回事。但是這是不對的。
同步包含阻塞和非阻塞
我們是用傳統(tǒng)的水壺?zé)T谒疅_之前我們一直做在水壺前面,等著水開。這就是阻塞的。
我們是用傳統(tǒng)的水壺?zé)T谒疅_之前我們先去客廳看電視了,但是水壺不會(huì)主動(dòng)通知我們, 需要我們時(shí)不時(shí)的去廚房看一下水有沒有燒開,這就是非阻塞的。
異步包含阻塞和非阻塞
我們是用帶有提醒功能的水壺?zé)T谒疅l(fā)出提醒之前我們一直做在水壺前面,等著水開。這就是阻塞的。
我們是用帶有提醒功能的水壺?zé)T谒疅l(fā)出提醒之前我們先去客廳看電視了,等水壺發(fā)出聲音提醒我們。這就是非阻塞的。
Unix中的五種I/O模型
對于一次I/O訪問(以read舉例),數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。 所以說,當(dāng)一個(gè)read操作發(fā)生時(shí),它會(huì)經(jīng)歷兩個(gè)階段:
第一階段:等待數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)。
第二階段:將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process)。
?
對于socket流而言
第一階段:通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),也就是被復(fù)制到內(nèi)核的某個(gè)緩沖區(qū)。
第二階段:把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)。
?
Unix下五種I/O模型:
同步阻塞I/O
同步非阻塞I/O
I/O多路復(fù)用(select和poll)
信號(hào)驅(qū)動(dòng)I/O(SIGIO)
異步非阻塞 IO
同步阻塞I/O
阻塞I/O下請求無法立即完成則保持阻塞,阻塞I/O分為如下兩個(gè)階段。
階段1:等待數(shù)據(jù)就緒。網(wǎng)絡(luò)I/O的情況就是等待遠(yuǎn)端數(shù)據(jù)陸續(xù)抵達(dá),也就是網(wǎng)絡(luò)數(shù)據(jù)被復(fù)制到內(nèi)核緩存區(qū)中,磁盤I/O的情況就是等待磁盤數(shù)據(jù)從磁盤上讀取到內(nèi)核態(tài)內(nèi)存中。
階段2:數(shù)據(jù)拷貝。出于系統(tǒng)安全,用戶態(tài)的程序沒有權(quán)限直接讀取內(nèi)核態(tài)內(nèi)存,因此內(nèi)核負(fù)責(zé)把內(nèi)核態(tài)內(nèi)存中的數(shù)據(jù)拷貝一份到用戶態(tài)內(nèi)存中。
這兩個(gè)階段必須都完成后才能繼續(xù)下一步操作
所以,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了。
同步非阻塞I/O
就是階段1的時(shí)候用戶進(jìn)程可選擇做其他事情,通過輪詢的方式看看內(nèi)核緩沖區(qū)是否就緒。如果數(shù)據(jù)就緒,再去執(zhí)行階段2。
也就是說非阻塞的recvform系統(tǒng)調(diào)用調(diào)用之后,進(jìn)程并沒有被阻塞,內(nèi)核馬上返回給進(jìn)程,如果數(shù)據(jù)還沒準(zhǔn)備好, 此時(shí)會(huì)返回一個(gè)error。進(jìn)程在返回之后,可以干點(diǎn)別的事情,然后再發(fā)起recvform系統(tǒng)調(diào)用。
重復(fù)上面的過程, 循環(huán)往復(fù)的進(jìn)行recvform系統(tǒng)調(diào)用。這個(gè)過程通常被稱之為輪詢。輪詢檢查內(nèi)核數(shù)據(jù),直到數(shù)據(jù)準(zhǔn)備好, 再拷貝數(shù)據(jù)到進(jìn)程,進(jìn)行數(shù)據(jù)處理。需要注意,第2階段的拷貝數(shù)據(jù)整個(gè)過程,進(jìn)程仍然是屬于阻塞的狀態(tài)。
在linux下,可以通過設(shè)置socket使其變?yōu)閚on-blocking。當(dāng)對一個(gè)non-blocking socket執(zhí)行讀操作時(shí),流程如圖所示:
所以,nonblocking IO的特點(diǎn)是用戶進(jìn)程需要不斷的主動(dòng)詢問kernel數(shù)據(jù)好了沒有。
I/O多路復(fù)用
我這里只想重點(diǎn)解釋一下I/O多路復(fù)用這種模型,因?yàn)楝F(xiàn)在用的最多。很多地方也稱為事件驅(qū)動(dòng)IO模型,只是叫法不同,意思都一個(gè)樣。
IO多路復(fù)用是指內(nèi)核一旦發(fā)現(xiàn)進(jìn)程指定的一個(gè)或者多個(gè)IO條件準(zhǔn)備讀取,它就通知該進(jìn)程。
目前支持I/O多路復(fù)用的系統(tǒng)調(diào)用有 select、pselect、poll、epoll,I/O多路復(fù)用就是通過一種機(jī)制,一個(gè)進(jìn)程可以監(jiān)視多個(gè)描述符, 一旦某個(gè)文件描述符fd就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。?
但select、pselect、poll、epoll本質(zhì)上都是同步I/O,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個(gè)讀寫過程是阻塞的, 而異步I/O則無需自己負(fù)責(zé)進(jìn)行讀寫,異步I/O的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
相比較于同步非阻塞I/O,它的改進(jìn)的地方在于,原來需要用戶進(jìn)程去輪詢的這事兒交給了內(nèi)核線程幫你完成, 而且這個(gè)內(nèi)核線程可以等待多個(gè)socket,能實(shí)現(xiàn)同時(shí)對多個(gè)IO端口進(jìn)行監(jiān)聽。
多路復(fù)用的特點(diǎn)是通過一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待IO文件描述符,內(nèi)核監(jiān)視這些文件描述符(套接字描述符), 其中的任意一個(gè)進(jìn)入讀就緒狀態(tài),select, poll,epoll函數(shù)就可以返回。對于監(jiān)視的方式, 又可以分為 select, poll, epoll三種方式。
所以,如果處理的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用多線程 + 阻塞IO的web server性能更好,可能延遲還更大。 也就是說,select/epoll的優(yōu)勢并不是對于單個(gè)連接能處理得更快,而是在于能處理更多的連接。
高并發(fā)的程序一般使用同步非阻塞方式而非多線程 + 同步阻塞方式。要理解這一點(diǎn),首先要扯到并發(fā)和并行的區(qū)別。 比如去某部門辦事需要依次去幾個(gè)窗口,辦事大廳里的人數(shù)就是并發(fā)數(shù),而窗口個(gè)數(shù)就是并行度。 也就是說并發(fā)數(shù)是指同時(shí)進(jìn)行的任務(wù)數(shù)(如同時(shí)服務(wù)的 HTTP 請求),而并行數(shù)是可以同時(shí)工作的物理資源數(shù)量(如 CPU 核數(shù))。?
通過合理調(diào)度任務(wù)的不同階段,并發(fā)數(shù)可以遠(yuǎn)遠(yuǎn)大于并行度,這就是區(qū)區(qū)幾個(gè) CPU 可以支持上萬個(gè)用戶并發(fā)請求的奧秘。 在這種高并發(fā)的情況下,為每個(gè)任務(wù)(用戶請求)創(chuàng)建一個(gè)進(jìn)程或線程的開銷非常大。而同步非阻塞方式可以把多個(gè) IO 請求丟到后臺(tái)去, 這就可以在一個(gè)進(jìn)程里服務(wù)大量的并發(fā) IO 請求。
IO多路復(fù)用歸為同步阻塞模式
異步非阻塞 IO
相對于同步IO,異步IO不是順序執(zhí)行。用戶進(jìn)程進(jìn)行aio_read系統(tǒng)調(diào)用之后,無論內(nèi)核數(shù)據(jù)是否準(zhǔn)備好,都會(huì)直接返回給用戶進(jìn)程, 然后用戶態(tài)進(jìn)程可以去做別的事情。等到socket數(shù)據(jù)準(zhǔn)備好了,內(nèi)核直接復(fù)制數(shù)據(jù)給進(jìn)程,然后從內(nèi)核向進(jìn)程發(fā)送通知。IO兩個(gè)階段, 進(jìn)程都是非阻塞的。
Linux提供了AIO庫函數(shù)實(shí)現(xiàn)異步,但是用的很少。目前有很多開源的異步IO庫,例如libevent、libev、libuv。異步過程如下圖所示:
更詳細(xì)的分析可參考?聊聊Linux5種IO模型
Java中四種I/O模型
上一章所述Unix中的五種I/O模型,除信號(hào)驅(qū)動(dòng)I/O外,Java對其它四種I/O模型都有所支持。
Java傳統(tǒng)IO模型即是同步阻塞I/O
NIO是同步非阻塞I/O
通過NIO實(shí)現(xiàn)的Reactor模式即是I/O多路復(fù)用模型的實(shí)現(xiàn)
通過AIO實(shí)現(xiàn)的Proactor模式即是異步I/O模型的實(shí)現(xiàn)
總結(jié)
以上是生活随笔為你收集整理的详解 Java 中 4 种 I/O 模型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈常见的七种加密算法及实现(附代码)
- 下一篇: 如何用九条命令在一分钟内检查 Linux