想理解Java的IO,不要从操作系统开始说起的都是耍流氓...
前言
在上一篇文章中,我們了解流的概念以及JavaIO流的基本用法,但JavaIO流的演化不僅是如此簡單,有心的讀者會發現,在JDK1.4之前的IO類都是基于阻塞的IO(可以從InputStream.read()方法實現中看到由synchronized修飾的代碼塊),發展到JDK1.4之后NIO提供了selector多路復用的機制以及channel和buffer,再到JDK1.7的NIO升級提供了真正的異步api......
Java網絡IO涵蓋的知識體系很廣泛,本文將簡單介紹Java網絡IO的相關知識:
(若文章有不正之處,或難以理解的地方,請多多諒解,歡迎指正)
?
?
從操作系統開始
為了保護操作系統的安全,會將內存分為用戶空間和內核空間兩個部分。如果用戶想要操作內核空間的數據,則需要把數據從內核空間拷貝到用戶空間。
舉個栗子,如果服務器收到了從客戶端過來的請求,并且想要進行處理,那么需要經過這幾個步驟:
-
服務器的網絡驅動接受到消息之后,向內核申請空間,并在收到完整的數據包(這個過程會產生延時,因為有可能是通過分組傳送過來的)后,將其復制到內核空間;
-
數據從內核空間拷貝到用戶空間;
-
用戶程序進行處理。
因此我們可以將服務器接收消息理解為兩個階段:
-
等待數據到達
-
將數據從內核空間拷貝到用戶空間
?
在操作系統中的IO
在此以Linux操作系統為例。Linux是一個將所有的外部設備都看作是文件來操作的操作系統,在它看來:everything is a file,那么我們就把對與外部設備的操作都看作是對文件進行操作。而且我們對一個文件進行讀寫,都需要通過調用內核提供的系統調用。
而在Linux中,一個基本的IO會涉及到兩個系統對象:一個是調用這個IO的進程對象(用戶進程),另一個是系統內核。也就是說,當一個read操作發生時,將會經歷這些階段:
-
通過read系統調用,向內核發送讀請求;
-
內核向硬件發送讀指令,并等待讀就緒;
-
DMA把將要讀取的數據復制到指定的內核緩存區中;
-
內核將數據從內核緩存區拷貝到用戶進程空間中。
在此期間會發生幾種IO操作:
-
同步IO:當用戶發出IO請求操作后,內核會去查看要讀取的數據是否就緒,如果沒有,就一直等待。期間用戶線程或內存會不斷地輪詢數據是否就緒。當數據就緒時,再把數據從內核拷貝到用戶空間。
-
異步IO:用戶線程只需發出IO請求和接收IO操作完成通知,期間的IO操作由內核自動完成,并發送通知告知用戶線程IO操作已經完成。也就是說,在異步IO中,并不會對用戶線程產生任何阻塞。
-
阻塞IO:當用戶線程發起一個IO請求操作,而內核要操作的數據還沒就緒,則當前線程被掛起,阻塞等待結果返回。
-
非阻塞IO:如果數據沒有就緒,就會返回一個標志信息告知用戶線程,當前的數據還沒有就緒。當前線程在獲得此次請求結果的過程中,還可以做點其他事情。
可能會有讀者覺得,怎么同步IO、異步IO和阻塞IO、非阻塞IO的操作好相似,為什么要它們都分出來呢?筆者認為,這同步、異步和阻塞、非阻塞是從不同角度來看待問題的。
?
同步與異步
同步與異步主要是從消息通知的角度來說的。
同步就是當一個任務A的完成需要依賴另一個任務B時,只有等到B任務完成后,A才能成功地進行,這是一種可靠的任務隊列。要么都成功,要么都失敗,兩個任務的狀態可以保持一致。
異步是不需要等待任務B完成,只是通知任務B要完成什么工作,任務A也立即執行,只要任務A自己執行完了那么整個任務就算完成了。至于任務B最終是否真正完成,A任務無法確定,所以這是不可靠的一種任務隊列。
舉個栗子,假如小J要去銀行柜臺辦事,拿號排隊。如果他只盯著號碼提示牌,還時不時問是否到他了,這就是同步;如果他拿了號之后就去打電話了,等到排到他的時候柜員通知他去辦理業務,這就是異步。他們之間的區別就在于,等待消息通知的方式不同。
阻塞與非阻塞
阻塞與非阻塞主要是從等待消息通知時的狀態角度來說的。
阻塞就是指在調用結果返回之前,當前線程會被掛起,一直處于等待消息通知的狀態,不能執行其他業務。只有當調用結果返回之后才能進行其他操作。
非阻塞與阻塞的概念相對應,就是指不能立即得到結果之前,該函數不會阻塞當前線程,而是會立即返回。雖然非阻塞的方式看上去可以明顯提高CPU的利用率,但是也會使系統的線程切換增加,需要好好評估增加的CPU執行時間能不能步長系統的切換成本。
我們繼續用上面的栗子,小J無論是在排隊還是拿號等通知,如果在這個等待的過程中,小J除了等待消息通知之外就做不了其他的事情,那么該機制就是阻塞的。如果他可以一邊打電話一邊等待,這個狀態就是非阻塞的。
同步、異步與阻塞、非阻塞
其實可能會有其他讀者把同步與阻塞等同起來,實際上這兩個是不同的。對于同步來說,很多時候當前線程還是在激活狀態,只是邏輯上當前函數沒有返回而已,此時,線程也會去處理其他的消息。也就是說,同步、阻塞其實是在消息通知機制下從不同角度對當前線程狀態的描述。
5.1 同步阻塞形式
這是效率最低的一種方式,拿上面的栗子來說,就是小J心無旁騖地排隊,什么別的事都不做。
在這里,同步與阻塞體現在:
-
同步:小J等待隊伍排到他辦理業務;
-
阻塞:小J在等待隊伍排到他的過程中,不做其他任務處理。
5.2 異步阻塞形式
如果小J在銀行等待辦理業務的時候,領了號,這時候就采用了異步的方式去等待消息被觸發(通知),等著柜員喊他的號而不是時刻盯著是不是排到他了。但是在這段時間里,他還是不能離開銀行去做其他的事情,那么很顯然,他被阻塞在這個等待喊號的操作上了。
在這里,異步與阻塞體現在:
-
異步:排到小J的話柜員會喊他的號碼;
-
阻塞:等待喊號的過程中,不能做其他事情。
5.3 同步非阻塞形式
實際上效率也是低下。小J在排隊的過程中可以打電話,但是要邊打電話邊看看還有多久才排到他。如果將打電話和觀察排隊情況看成是程序中的兩個操作的話,這個程序需要在這兩個不同的行為之間來回切換。
在這里,同步與非阻塞體現在:
-
同步:排隊等待輪到他辦理業務;
-
非阻塞:可以在排隊的過程中打電話,只不過要時不時看看還要多久才排到他辦理業務。
5.4 異步非阻塞形式
這是一個效率更高的模式。小J在拿號之后可以去打電話,只要等待柜員喊號就可以了,在這里打電話是等待者的事情,而通知小J辦理業務是柜員的事情。
在這里,異步和非阻塞體現在:
-
異步:柜員喊小J去辦理業務;
-
非阻塞:在等待喊號的過程中,小J去打電話,只要接收到柜員喊號的通知即可,無需關注是否隊伍的進度。
也就是說,同步和異步僅需關注消息如何通知的機制,而阻塞和非阻塞關注的是在等待消息通知的過程中能不能去做別的事。在同步情況下,是由處理者自己去等待消息是否被觸發,而異步情況下是由觸發機制來通知處理者處理業務。
Linux的五種IO模型
在我們了解Linux操作系統的IO操作,以及同步與異步、阻塞與非阻塞的概念之后,我們來看看Linux系統中根據同步、異步、阻塞、非阻塞實現的五種IO模型。以Linux下的系統調用recv為例,是一個用于從套接字上接收一個消息,因為是系統調用,所以在調用的時候,會從用戶空間切換到內核空間運行一段時間后,再切換回來。在默認情況下recv會等到網絡數據到達并復制到用戶空間或發生錯誤時返回。
6.1 同步阻塞IO模型
從系統調用recv到將數據從內核復制到用戶空間并返回,在這段時間內進程始終阻塞。就相當于,小J想去柜臺辦理業務,如果柜臺業務繁忙,他也要排隊,直到排到他辦理完業務,才能去做別的事。顯然,這個IO模型是同步且阻塞的。
6.2 同步非阻塞IO模型
在這里recv不管有沒有獲得到數據都返回,如果沒有數據的話就過段時間再調用recv看看,如此循環。就像是小J來柜臺辦理業務,發現柜員休息,他離開了,過一會又過來看看營業了沒,直到終于碰到柜員營業了,這才辦理了業務。而小J在中間離開的時間,可以做他自己的事情。但是這個模型只有在檢查無數據的時候是非阻塞的,在數據到達的時候依然要等待復制數據到用戶空間(辦理業務),因此它還是同步IO。
6.3 IO復用模型
在IO復用模型中,調用recv之前會先調用select或poll,這兩個系統調用都可以在內核準備好數據(網絡數據已經到達內核了)時告知用戶進程,它準備好了,這時候再調用recv時是一定有數據的。因此在這一模型中,進程阻塞于select或poll,而沒有阻塞在recv上。就相當于,小J來銀行辦理業務,大堂經理告訴他現在所有柜臺都有人在辦理業務,等有空位再告訴他。于是小J就等啊等(select或poll調用中),過了一會兒大堂經理告訴他有柜臺空出來可以辦理業務了,但是具體是幾號柜臺,你自己找下吧,于是小J就只能挨個柜臺地找。
6.4 信號驅動IO模型
此處會通過調用sigaction注冊信號函數,在內核數據準備好的時候系統就中斷當前程序,執行信號函數(在這里調用recv)。相當于,小J讓大堂經理在柜臺有空位的時候通知他(注冊信號函數),等沒多久大堂經理通知他,因為他是銀行的VIPPP會員,所以專門給他開了一個柜臺來辦理業務,小J就去特席柜臺辦理業務了。但即使在等待的過程中是非阻塞的,但在辦理業務的過程中依然是同步的。

6.5 異步IO模型
調用aio_read令內核把數據準備好,并且復制到用戶進程空間后執行事先指定好的函數。就像是,小J交代大堂經理把業務給辦理好了就通知他來驗收,在這個過程中小J可以去做自己的事情。這就是真正的異步IO。

我們可以看到,前四種模型都是屬于同步IO,因為在內核數據復制到用戶空間的這一過程都是阻塞的。而最后一種異步IO,通過將IO操作交給操作系統處理,當前進程不關心具體IO的實現,后來再通過回調函數,或信號量通知當前進程直接對IO返回結果進行處理。
BIO、NIO、AIO的區別
上文談到IO的四種模式:同步阻塞IO、同步非阻塞IO、異步阻塞IO、異步非阻塞IO,在JavaIO中提供了三種模式的實現:BIO(同步阻塞IO)、NIO(同步非阻塞IO)、AIO(異步非阻塞IO)。至于這四種模式之間的區別,上文已經有較為詳細的介紹了,接下來筆者將對這三種JavaIO類型之間的區別進行介紹。
-
BIO:同步并阻塞,在服務器中實現的模式為一個連接一個線程。也就是說,客戶端有連接請求的時候,服務器就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然這也可以通過線程池機制改善。BIO一般適用于連接數目小且固定的架構,這種方式對于服務器資源要求比較高,而且并發局限于應用中,是JDK1.4之前的唯一選擇,但好在程序直觀簡單,易理解。
-
NIO:同步并非阻塞,在服務器中實現的模式為一個請求一個線程,也就是說,客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到有連接IO請求時才會啟動一個線程進行處理。NIO一般適用于連接數目多且連接比較短(輕操作)的架構,并發局限于應用中,編程比較復雜,從JDK1.4開始支持。
-
AIO:異步并非阻塞,在服務器中實現的模式為一個有效請求一個線程,也就是說,客戶端的IO請求都是通過操作系統先完成之后,再通知服務器應用去啟動線程進行處理。AIO一般適用于連接數目多且連接比較長(重操作)的架構,充分調用操作系統參與并發操作,編程比較復雜,從JDK1.7開始支持。
?
結語
本文從操作系統進行文件讀寫入手,對同步、異步、阻塞、非阻塞以及它們組合而成的IO模式進行了介紹,還了解Linux操作系統中的五種IO模型,以及重新回到JavaIO,看待BIO、NIO、AIO之間的區別。
如果本文對你有幫助,請給一個贊吧,這會是我最大的動力~
?
參考資料:
https://www.cnblogs.com/felixzh/p/10345929.html
總結
以上是生活随笔為你收集整理的想理解Java的IO,不要从操作系统开始说起的都是耍流氓...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 干货 | DevSecOps在携程的最佳
- 下一篇: 戳破微服务的七大谎言