1. 程式人生 > >Java NIO:淺析I/O模型

Java NIO:淺析I/O模型

問題 區別 ror borde ket .cn dex selector 以及

學習Java的同學註意了!!!

學習過程中遇到什麽問題或者想獲取學習資源的話,歡迎加入Java學習交流群:618528494 我們一起學Java!

  也許很多朋友在學習NIO的時候都會感覺有點吃力,對裏面的很多概念都感覺不是那麽明朗。在進入Java NIO編程之前,我們今天先來討論一些比較基礎的知識:I/O模型。下面本文先從同步和異步的概念 說起,然後接著闡述了阻塞和非阻塞的區別,接著介紹了阻塞IO和非阻塞IO的區別,然後介紹了同步IO和異步IO的區別,接下來介紹了5種IO模型,最後介紹了兩種和高性能IO設計相關的設計模式(Reactor和Proactor)。

  以下是本文的目錄大綱:

  一.什麽是同步?什麽是異步?

  二.什麽是阻塞?什麽是非阻塞?

  三.什麽是阻塞IO?什麽是非阻塞IO?

  四.什麽是同步IO?什麽是異步IO?

  五.五種IO模型

  六.兩種高性能IO設計模式

  若有不正之處,請多多諒解並歡迎批評指正。

一.什麽是同步?什麽是異步?

  同步和異步的概念出來已經很久了,網上有關同步和異步的說法也有很多。以下是我個人的理解:

  同步就是:如果有多個任務或者事件要發生,這些任務或者事件必須逐個地進行,一個事件或者任務的執行會導致整個流程的暫時等待,這些事件沒有辦法並發地執行;

  異步就是:如果有多個任務或者事件發生,這些事件可以並發地執行,一個事件或者任務的執行不會導致整個流程的暫時等待。

  這就是同步和異步。舉個簡單的例子,假如有一個任務包括兩個子任務A和B,對於同步來說,當A在執行的過程中,B只有等待,直至A執行完畢,B才能執行;而對於異步就是A和B可以並發地執行,B不必等待A執行完畢之後再執行,這樣就不會由於A的執行導致整個任務的暫時等待。

  如果還不理解,可以先看下面這2段代碼:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 void fun1() { } void fun2() { } void function(){ fun1(); fun2()
..... ..... }

  這段代碼就是典型的同步,在方法function中,fun1在執行的過程中會導致後續的fun2無法執行,fun2必須等待fun1執行完畢才可以執行。

  接著看下面這段代碼:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void fun1() { } void fun2() { } void function(){ new Thread(){ public void run() { fun1(); } }.start(); new Thread(){ public void run() { fun2(); } }.start(); ..... ..... }

  這段代碼是一種典型的異步,fun1的執行不會影響到fun2的執行,並且fun1和fun2的執行不會導致其後續的執行過程處於暫時的等待。

  事實上,同步和異步是一個非常廣的概念,它們的重點在於多個任務和事件發生時,一個事件的發生或執行是否會導致整個流程的暫時等待。我覺得可以將同步和異步與Java中的synchronized關鍵字聯系起來進行類比。當多個線程同時訪問一個變量時,每個線程訪問該變量就是一個事件,對於同步來說,就是這些線程必須逐個地來訪問該變量,一個線程在訪問該變量的過程中,其他線程必須等待;而對於異步來說,就是多個線程不必逐個地訪問該變量,可以同時進行訪問。

  因此,個人覺得同步和異步可以表現在很多方面,但是記住其關鍵在於多個任務和事件發生時,一個事件的發生或執行是否會導致整個流程的暫時等待。一般來說,可以通過多線程的方式來實現異步,但是千萬記住不要將多線程和異步畫上等號,異步只是宏觀上的一個模式,采用多線程來實現異步只是一種手段,並且通過多進程的方式也可以實現異步。

二.什麽是阻塞?什麽是非阻塞?

  在前面介紹了同步和異步的區別,這一節來看一下阻塞和非阻塞的區別。

  阻塞就是:當某個事件或者任務在執行過程中,它發出一個請求操作,但是由於該請求操作需要的條件不滿足,那麽就會一直在那等待,直至條件滿足;

  非阻塞就是:當某個事件或者任務在執行過程中,它發出一個請求操作,如果該請求操作需要的條件不滿足,會立即返回一個標誌信息告知條件不滿足,不會一直在那等待。

  這就是阻塞和非阻塞的區別。也就是說阻塞和非阻塞的區別關鍵在於當發出請求一個操作時,如果條件不滿足,是會一直等待還是返回一個標誌信息。

  舉個簡單的例子:

  假如我要讀取一個文件中的內容,如果此時文件中沒有內容可讀,對於同步來說就是會一直在那等待,直至文件中有內容可讀;而對於非阻塞來說,就會直接返回一個標誌信息告知文件中暫時無內容可讀。

  在網上有一些朋友將同步和異步分別與阻塞和非阻塞畫上等號,事實上,它們是兩組完全不同的概念。註意,理解這兩組概念的區別對於後面IO模型的理解非常重要。

  同步和異步著重點在於多個任務的執行過程中,一個任務的執行是否會導致整個流程的暫時等待;

  而阻塞和非阻塞著重點在於發出一個請求操作時,如果進行操作的條件不滿足是否會返會一個標誌信息告知條件不滿足。

  理解阻塞和非阻塞可以同線程阻塞類比地理解,當一個線程進行一個請求操作時,如果條件不滿足,則會被阻塞,即在那等待條件滿足。

三.什麽是阻塞IO?什麽是非阻塞IO?

  在了解阻塞IO和非阻塞IO之前,先看下一個具體的IO操作過程是怎麽進行的。

  通常來說,IO操作包括:對硬盤的讀寫、對socket的讀寫以及外設的讀寫。

  當用戶線程發起一個IO請求操作(本文以讀請求操作為例),內核會去查看要讀取的數據是否就緒,對於阻塞IO來說,如果數據沒有就緒,則會一直在那等待,直到數據就緒;對於非阻塞IO來說,如果數據沒有就緒,則會返回一個標誌信息告知用戶線程當前要讀的數據沒有就緒。當數據就緒之後,便將數據拷貝到用戶線程,這樣才完成了一個完整的IO讀請求操作,也就是說一個完整的IO讀請求操作包括兩個階段:

  1)查看數據是否就緒;

  2)進行數據拷貝(內核將數據拷貝到用戶線程)。

  那麽阻塞(blocking IO)和非阻塞(non-blocking IO)的區別就在於第一個階段,如果數據沒有就緒,在查看數據是否就緒的過程中是一直等待,還是直接返回一個標誌信息。

  Java中傳統的IO都是阻塞IO,比如通過socket來讀數據,調用read()方法之後,如果數據沒有就緒,當前線程就會一直阻塞在read方法調用那裏,直到有數據才返回;而如果是非阻塞IO的話,當數據沒有就緒,read()方法應該返回一個標誌信息,告知當前線程數據沒有就緒,而不是一直在那裏等待。

四.什麽是同步IO?什麽是異步IO?

  我們先來看一下同步IO和異步IO的定義,在《Unix網絡編程》一書中對同步IO和異步IO的定義是這樣的:

  A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
  An asynchronous I/O operation does not cause the requesting process to be blocked.

  從字面的意思可以看出:同步IO即 如果一個線程請求進行IO操作,在IO操作完成之前,該線程會被阻塞;

  而異步IO為 如果一個線程請求進行IO操作,IO操作不會導致請求線程被阻塞。

  事實上,同步IO和異步IO模型是針對用戶線程和內核的交互來說的:

  對於同步IO:當用戶發出IO請求操作之後,如果數據沒有就緒,需要通過用戶線程或者內核不斷地去輪詢數據是否就緒,當數據就緒時,再將數據從內核拷貝到用戶線程;

  而異步IO:只有IO請求操作的發出是由用戶線程來進行的,IO操作的兩個階段都是由內核自動完成,然後發送通知告知用戶線程IO操作已經完成。也就是說在異步IO中,不會對用戶線程產生任何阻塞。

  這是同步IO和異步IO關鍵區別所在,同步IO和異步IO的關鍵區別反映在數據拷貝階段是由用戶線程完成還是內核完成。所以說異步IO必須要有操作系統的底層支持。

  註意同步IO和異步IO與阻塞IO和非阻塞IO是不同的兩組概念。

  阻塞IO和非阻塞IO是反映在當用戶請求IO操作時,如果數據沒有就緒,是用戶線程一直等待數據就緒,還是會收到一個標誌信息這一點上面的。也就是說,阻塞IO和非阻塞IO是反映在IO操作的第一個階段,在查看數據是否就緒時是如何處理的。

五.五種IO模型

  在《Unix網絡編程》一書中提到了五種IO模型,分別是:阻塞IO、非阻塞IO、多路復用IO、信號驅動IO以及異步IO。

  下面就分別來介紹一下這5種IO模型的異同。

1.阻塞IO模型

  最傳統的一種IO模型,即在讀寫數據過程中會發生阻塞現象。

  當用戶線程發出IO請求之後,內核會去查看數據是否就緒,如果沒有就緒就會等待數據就緒,而用戶線程就會處於阻塞狀態,用戶線程交出CPU。當數據就緒之後,內核會將數據拷貝到用戶線程,並返回結果給用戶線程,用戶線程才解除block狀態。

  典型的阻塞IO模型的例子為:

1 data = socket.read();

  如果數據沒有就緒,就會一直阻塞在read方法。

2.非阻塞IO模型

  當用戶線程發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。如果結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作。一旦內核中的數據準備好了,並且又再次收到了用戶線程的請求,那麽它馬上就將數據拷貝到了用戶線程,然後返回。

  所以事實上,在非阻塞IO模型中,用戶線程需要不斷地詢問內核數據是否就緒,也就說非阻塞IO不會交出CPU,而會一直占用CPU。

  典型的非阻塞IO模型一般如下:

1 2 3 4 5 6 7 while(true){ data = socket.read(); if(data!= error){ 處理數據 break; } }

  但是對於非阻塞IO就有一個非常嚴重的問題,在while循環中需要不斷地去詢問內核數據是否就緒,這樣會導致CPU占用率非常高,因此一般情況下很少使用while循環這種方式來讀取數據。

3.多路復用IO模型

  多路復用IO模型是目前使用得比較多的模型。Java NIO實際上就是多路復用IO。

  在多路復用IO模型中,會有一個線程不斷去輪詢多個socket的狀態,只有當socket真正有讀寫事件時,才真正調用實際的IO讀寫操作。因為在多路復用IO模型中,只需要使用一個線程就可以管理多個socket,系統不需要建立新的進程或者線程,也不必維護這些線程和進程,並且只有在真正有socket讀寫事件進行時,才會使用IO資源,所以它大大減少了資源占用。

  在Java NIO中,是通過selector.select()去查詢每個通道是否有到達事件,如果沒有事件,則一直阻塞在那裏,因此這種方式會導致用戶線程的阻塞。

  也許有朋友會說,我可以采用 多線程+ 阻塞IO 達到類似的效果,但是由於在多線程 + 阻塞IO 中,每個socket對應一個線程,這樣會造成很大的資源占用,並且尤其是對於長連接來說,線程的資源一直不會釋放,如果後面陸續有很多連接的話,就會造成性能上的瓶頸。

  而多路復用IO模式,通過一個線程就可以管理多個socket,只有當socket真正有讀寫事件發生才會占用資源來進行實際的讀寫操作。因此,多路復用IO比較適合連接數比較多的情況。

  另外多路復用IO為何比非阻塞IO模型的效率高是因為在非阻塞IO中,不斷地詢問socket狀態時通過用戶線程去進行的,而在多路復用IO中,輪詢每個socket狀態是內核在進行的,這個效率要比用戶線程要高的多。

  不過要註意的是,多路復用IO模型是通過輪詢的方式來檢測是否有事件到達,並且對到達的事件逐一進行響應。因此對於多路復用IO模型來說,一旦事件響應體很大,那麽就會導致後續的事件遲遲得不到處理,並且會影響新的事件輪詢。

4.信號驅動IO模型

  在信號驅動IO模型中,當用戶線程發起一個IO請求操作,會給對應的socket註冊一個信號函數,然後用戶線程會繼續執行,當內核數據就緒時會發送一個信號給用戶線程,用戶線程接收到信號之後,便在信號函數中調用IO讀寫操作來進行實際的IO請求操作。

5.異步IO模型

  異步IO模型才是最理想的IO模型,在異步IO模型中,當用戶線程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從內核的角度,當它受到一個asynchronous read之後,它會立刻返回,說明read請求已經成功發起了,因此不會對用戶線程產生任何block。然後,內核會等待數據準備完成,然後將數據拷貝到用戶線程,當這一切都完成之後,內核會給用戶線程發送一個信號,告訴它read操作完成了。也就說用戶線程完全不需要實際的整個IO操作是如何進行的,只需要先發起一個請求,當接收內核返回的成功信號時表示IO操作已經完成,可以直接去使用數據了。

  也就說在異步IO模型中,IO操作的兩個階段都不會阻塞用戶線程,這兩個階段都是由內核自動完成,然後發送一個信號告知用戶線程操作已完成。用戶線程中不需要再次調用IO函數進行具體的讀寫。這點是和信號驅動模型有所不同的,在信號驅動模型中,當用戶線程接收到信號表示數據已經就緒,然後需要用戶線程調用IO函數進行實際的讀寫操作;而在異步IO模型中,收到信號表示IO操作已經完成,不需要再在用戶線程中調用iO函數進行實際的讀寫操作。

  註意,異步IO是需要操作系統的底層支持,在Java 7中,提供了Asynchronous IO。

  前面四種IO模型實際上都屬於同步IO,只有最後一種是真正的異步IO,因為無論是多路復用IO還是信號驅動模型,IO操作的第2個階段都會引起用戶線程阻塞,也就是內核進行數據拷貝的過程都會讓用戶線程阻塞。

六.兩種高性能IO設計模式

  在傳統的網絡服務設計模式中,有兩種比較經典的模式:

  一種是 多線程,一種是線程池。

  對於多線程模式,也就說來了client,服務器就會新建一個線程來處理該client的讀寫事件,如下圖所示:

技術分享

  這種模式雖然處理起來簡單方便,但是由於服務器為每個client的連接都采用一個線程去處理,使得資源占用非常大。因此,當連接數量達到上限時,再有用戶請求連接,直接會導致資源瓶頸,嚴重的可能會直接導致服務器崩潰。

  因此,為了解決這種一個線程對應一個客戶端模式帶來的問題,提出了采用線程池的方式,也就說創建一個固定大小的線程池,來一個客戶端,就從線程池取一個空閑線程來處理,當客戶端處理完讀寫操作之後,就交出對線程的占用。因此這樣就避免為每一個客戶端都要創建線程帶來的資源浪費,使得線程可以重用。

  但是線程池也有它的弊端,如果連接大多是長連接,因此可能會導致在一段時間內,線程池中的線程都被占用,那麽當再有用戶請求連接時,由於沒有可用的空閑線程來處理,就會導致客戶端連接失敗,從而影響用戶體驗。因此,線程池比較適合大量的短連接應用。

  因此便出現了下面的兩種高性能IO設計模式:Reactor和Proactor。

  在Reactor模式中,會先對每個client註冊感興趣的事件,然後有一個線程專門去輪詢每個client是否有事件發生,當有事件發生時,便順序處理每個事件,當所有事件處理完之後,便再轉去繼續輪詢,如下圖所示:

技術分享

  從這裏可以看出,上面的五種IO模型中的多路復用IO就是采用Reactor模式。註意,上面的圖中展示的 是順序處理每個事件,當然為了提高事件處理速度,可以通過多線程或者線程池的方式來處理事件。

  在Proactor模式中,當檢測到有事件發生時,會新起一個異步操作,然後交由內核線程去處理,當內核線程完成IO操作之後,發送一個通知告知操作已完成,可以得知,異步IO模型采用的就是Proactor模式。

學習Java的同學註意了!!!

學習過程中遇到什麽問題或者想獲取學習資源的話,歡迎加入Java學習交流群:618528494 我們一起學Java!

Java NIO:淺析I/O模型