Java IO對大多數Java程式設計師來說是熟悉又陌生,熟悉的是感覺到處都有它的身影,小到簡單的讀取檔案,大到各種伺服器的應用,陌生的是Java IO背後到底是一個怎樣的機制,今天就讓我們去了解一下這位老朋友吧。本文不講解Java IO如何具體使用,有這方面需求的同學可以自己查下。
IO模型
要說IO,就不得不說IO模型,IO模型大家都有所瞭解,同步非同步,阻塞非阻塞什麼的,總的來說IO模型可分為以下五種:
- 阻塞IO
- 非阻塞IO
- 多路複用IO
- 訊號驅動IO
- 非同步IO
那麼這幾種IO都有什麼區別呢?下面我們一一來看,每種模型我都會舉一個適當的例子助於理解:
1.阻塞IO
阻塞IO相信大家都最熟悉了,執行緒發起一個IO請求,直到有結果返回,否則則一直阻塞等待,比如我們平常常見的阻塞資料庫操作,網路IO等。
小明阻塞IO吃飯:
五年前一天週末,小明和朋友一起去商場的外婆家吃飯,到店後發現排隊的人超多,所以他就領了一個號碼,然後他和朋友就坐在旁邊等候,一直等著服務員叫他們的號,也不能做其他事,過了一個多小時終於輪到他們了,然後他們進店點菜,又得等待上菜,最後他們吃飯總共花了兩個小時;
關鍵部分:
- 等待座位吃飯:一直阻塞,直到有座位
- 等待上菜:一直阻塞,直到有菜(假設菜上齊了再吃)
沒什麼說的,反正就是一直等,反應到程式中就是一直阻塞,而一個IO請求需要一個執行緒,可想而知當有大量的IO請求,執行緒的建立和銷燬,執行緒間的切換,執行緒所佔用的資源等等要耗費多少時間和資源,系統的效能會有多差。
2.非阻塞IO
非阻塞IO和阻塞IO的最大區別就在於執行緒發起一個IO請求,不會一直堵塞直到有資料,而是不斷的檢查是否已有資料,若有資料則讀取資料。
小明非阻塞IO吃飯:
有了第一次的教訓,小明學乖了,他在拿到後不再傻傻的等著,而是去外婆家旁邊逛了逛,每過3分鐘他就會回來,然後跑到前臺去詢問服務員輪到他了嗎?不幸的是,排隊的人超多,直到過了半個多小時後才輪到他進店吃飯,期間他大概問了十幾次,他們進店點菜,又得等待上菜,最後他們吃飯總共花了兩個小時,基本也沒做啥其他事;
關鍵部分:
- 領號後詢問是否輪到他:非阻塞,非詢問期間可以做點別的事,但也不做了啥大事
- 等待上菜:一直阻塞,直到有菜(假設菜上齊了再吃)
總的來說非阻塞IO的非阻塞主要體現在不需要一直等待到有資料,當然讀資料那部分操作還是阻塞的,另外這種非阻塞模式需要使用者執行緒自己不斷詢問檢查,其實效率也不是太高,實際程式設計中運用的也不多。
3.多路複用IO
既然上面我們說到非阻塞IO的缺點,那麼有沒有什麼方式改進呢?答案是當然有,那就是多路複用IO,我理解的它的特點就是複用,首先它也是一種非阻塞IO的模型,只不過上面說到輪詢的方式用了不同的方式處理了,當一個執行緒發起IO請求,系統會將它註冊到一個單獨管理IO請求的一個執行緒,之後該IO的相關操作的通知狀態都有這個管理IO請求的執行緒處理,Java 1.4釋出的NIO就是這種模式,我們可以大致來看一下它的流程:
// 開啟伺服器套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 伺服器配置為非阻塞
ssc.configureBlocking(false);
// 進行服務的繫結
ssc.bind(new InetSocketAddress("localhost", 8008));
// 這裡的selector就相當於單獨管理IO請求的執行緒
Selector selector = Selector.open();
// 註冊到selector,等待連線
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); //為IO請求去輪詢狀態
Set<SelectionKey> keys = selector.selectedKeys(); //多個IO請求的狀態
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) { //依次處理IO請求
SelectionKey key = keyIterator.next();
doThing(key)
...
}
}
可以看出Java NIO的模式就是多路複用IO模型的應用。
小明多路複用IO吃飯:
隨著生意越來越好,外婆家發現好多顧客都堵在門口等待吃飯,等待區都站不下來人了,,思來想去,外婆家準備請一個人專門來維護顧客的排隊請求,這樣顧客取號後,就不用堵在門口了,我們叫他小A,小明這次取號後,將自己的相關資訊告訴小A,並從小A那裡獲得了一個GPS(用於小A能快速找到小明,假設有了GPS後,小A能秒速找到小明),然後小明就跟朋友們開心的去逛商場,看看MM,買買衣服,而小A則不斷的觀察店裡的情況,當有空座位出現的時候,他便會按照相關資訊找到具體的顧客,將其帶回進行用餐,但他們進店點菜,還得等待上菜,最後他們吃飯總共花了兩個小時,但是他們不再需要排隊等位,而是去做一些其他的事。
關鍵部分:
- 領號後委託給小A,小A觀察到有空位後帶回小明:非阻塞,領號後可以安心去做自己的事,不用擔心錯過
- 等待上菜:一直阻塞,直到有菜(假設菜上齊了再吃)
多路複用IO可以看成普通非阻塞IO的升級版,也是目前Java程式設計中用到比較多的IO模型,它的優勢在於可以處理大量的IO請求,用一個執行緒管理所有的IO請求,無需像阻塞IO和非阻塞IO一樣,每個IO需要一個執行緒處理,提升了系統的吞吐量。
4.訊號驅動IO
訊號驅動IO相對於以上幾種模型最大的特點就是它支援核心訊號通知,執行緒在發起一個IO請求後,會註冊一個訊號函式,然後核心在確認資料可讀了,便會給相應的執行緒傳送通知,讓其進行具體IO讀寫操作。
小明訊號驅動IO吃飯:
又了一段時間,外婆家通過使用複用IO模式緩解了排隊擁擠的情況,但是覺得還要請一個人專門維護佇列,感覺不划算,那麼有沒有一種更好的方式呢?經過一天的苦思冥想,外婆家的經理又想出一個好辦法,讓每個顧客在領完號後,關注一下外婆家的公眾號,然後顧客就可以去做別的事了,定時或者當排隊資訊發生改變時給顧客傳送通知,告知他現在的排隊序號或者輪到他吃飯了,顧客可以根據相應的資訊做相應的行為,比如快輪到了就開始往店裡走(實際程式中並不一定有這種狀態,這裡只是大概模擬),或者輪到自己瞭然後進店吃飯,他們仍然不用排隊等位,而是去做一些其他的事。
關鍵部分:
- 領號後關注公眾號,註冊關係:非阻塞,領號後可以安心去做自己的事,不用擔心錯過
- 等待上菜:一直阻塞,直到有菜(假設菜上齊了再吃)
就實際來說,訊號驅動IO用的並不多,因為訊號驅動IO底層是使用SIGIO訊號,所以它主要使用在UDP協議上,因為UDP產生SIGIO訊號的時候只有兩種可能:
- 1.要麼資料到達
- 2.發生錯誤
但相對TCP來說,產生SIGIO訊號的地方太多了,比如請求連線,確認,斷開,錯誤等等,所以我們很難根據SIGIO訊號判斷到底發生了什麼。
5.非同步IO
以上四種IO其實都還是同步IO,因為它們在讀寫資料時都是阻塞的,非同步IO相較於它們最大的特點是它讀寫資料的時候也是非阻塞的,使用者執行緒在發起一個IO請求的時候,除了給核心執行緒傳遞具體的IO請求外,還會給其傳遞資料緩衝區,回撥函式通知等內容,然後使用者執行緒就繼續執行,等到核心執行緒發起相應通知的時候,說明資料已經準備就緒,使用者執行緒直接使用即可,無需再阻塞從核心拷貝資料到使用者執行緒。
小明非同步IO吃飯:
有過了一段時間,小明又想吃外婆家了,但是這個週末他並不想出門,他突然在網上看到新聞說外婆家竟然可以叫外賣,小明高興壞了,他馬上打電話給外婆家,告訴它自己想要吃哪些菜(相當於IO請求所需要的資料),然後將自己的聯絡號碼(相當於回撥通知)和住址(相當於資料緩衝區)也告訴它,然後就掛掉電話,開心的做去打遊戲了,過了半個小時後,手機響起,告知外賣已經到了,小明開門取外賣就可以直接開吃了。整個過程小明直到吃飯都沒有等待阻塞。
關鍵部分:
- 叫外賣並提供相應的資訊:非阻塞,打完電話後做自己的事
- 通知外賣到了:直接開門取外賣直接開吃,非阻塞
我們可以看出,非同步IO才是真正的非同步,因為它連資料拷貝這個過程都是非阻塞的,使用者執行緒根本不用關心資料的讀寫等操作,只需等待核心執行緒通知後,直接處理資料即可,當然非同步IO需要系統核心支援,比如Linux中的AIO和Windows中的IOCP,但是也可以通過多執行緒跟阻塞I/O模擬非同步IO,比如可以在多路複用IO模型上進行相應的改變,另外也有現有的實現,比如非同步I/O的庫:libeio
最後用一張圖總體概括一下Java IO(圖片來自美團技術部落格):
Java IO概圖:
多路複用IO在Linux中的實現
因為後續會講到Java NIO,所以我們需要了解作業系統是如何支援多路複用IO的,Linux中支援支援三種多路IO複用機制,分別是select、poll和epoll,本來這裡我想自己寫的,但查閱了相應的一些資料後,發現自己的水平還是不夠,這裡我不準備班門弄斧了,因為我找到了很多寫的比較好的文章,這裡就給大家列一下,僅供參考:
總結
這篇文章主要講了最基礎的IO模型,不過我認為最基礎的往往是最重要的,只有理解了基礎的原理,才能對基於它們實現的類庫或者工具有更加深刻的認識,下一篇文章將會主要講一下基於多路複用IO的Java NIO,敬請期待。