Java NIO1:淺談I/O模型

1、什麼是同步?什麼是異步?編程

     同步和異步的概念出來已經好久了,網上有關同步和異步的說法也有不少。如下是我我的的理解:設計模式

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

  異步就是:若是有多個任務或者事件發生,這些事件能夠併發地執行,一個事件或者任務的執行不會致使整個流程的暫時等待。網絡

     這就是同步和異步。舉個簡單的例子,假若有一個任務包括兩個子任務A和B,對於同步來講,當A在執行的過程當中,B只有等待,直至A執行完畢,B才能執行;而對於異步就是A和B能夠併發地執行,B沒必要等待A執行完畢以後再執行,這樣就不會因爲A的執行致使整個任務的暫時等待。多線程

若是還不理解,能夠先看下面這2段代碼:併發

void fun1() {
        
} 
    
void fun2() {
        
} 
    
void function(){
    fun1(); 
    fun2(); 
    ..... 
    ..... 
} 

這段代碼就是典型的同步,在方法function中,fun1在執行的過程當中會致使後續的fun2沒法執行,fun2必須等待fun1執行完畢才能夠執行。異步

接着看下面這段代碼:socket

void fun1() { 
      
} 
  
void fun2() { 
      
} 
  
void function(){ 
    new Thread(){ 
        public void run() { 
            fun1(); 
        } 
    }.start(); 
      
    new Thread(){ 
        public void run() { 
            fun2(); 
        } 
    }.start(); 
 
    ..... 
    ..... 
}

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

   事實上,同步和異步是一個很是廣的概念,它們的重點在於多個任務和事件發生時,一個事件的發生或執行是否會致使整個流程的暫時等待。我以爲能夠將同步和異步與Java中的synchronized關鍵字聯繫起來進行類比。當多個線程同時訪問一個變量時,每一個線程訪問該變量就是一個事件,對於同步來講,就是這些線程必須逐個地來訪問該變量,一個線程在訪問該變量的過程當中,其餘線程必須等待;而對於異步來講,就是多個線程沒必要逐個地訪問該變量,能夠同時進行訪問。函數

  所以,我的以爲同步和異步能夠表如今不少方面,可是記住其關鍵在於多個任務和事件發生時,一個事件的發生或執行是否會致使整個流程的暫時等待。通常來講,能夠經過多線程的方式來實現異步,可是千萬記住不要將多線程和異步畫上等號,異步只是宏觀上的一個模式,採用多線程來實現異步只是一種手段,而且經過多進程的方式也能夠實現異步。

2、什麼是阻塞?什麼是非阻塞?

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

  阻塞就是:當某個事件或者任務在執行過程當中,它發出一個請求操做,可是因爲該請求操做須要的條件不知足,那麼就會一直在那等待,直至條件知足;

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

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

  舉個簡單的例子:

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

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

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

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

  理解阻塞和非阻塞能夠同線程阻塞類比地理解,當一個線程進行一個請求操做時,若是條件不知足,則會被阻塞,即在那等待條件知足。

3、什麼是阻塞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()方法應該返回一個標誌信息,告知當前線程數據沒有就緒,而不是一直在那裏等待。

4、什麼是同步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操做的第一個階段,在查看數據是否就緒時是如何處理的。

5、五種IO模型

在《Unix網絡編程》一書中提到了五種IO模型,分別是:阻塞IO、非阻塞IO、多路複用IO、信號驅動IO以及異步IO。下面就分別來介紹一下這5種IO模型的異同。

一、阻塞IO模型

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

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

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

data = socket.read(); 

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

二、非阻塞IO模型

     當用戶線程發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。若是結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦內核中的數據準備好了,而且又再次收到了用戶線程的請求,那麼它立刻就將數據拷貝到了用戶線程,而後返回。

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

     典型的非阻塞IO模型通常以下:

while(true){ 
    data = socket.read(); 
    if(data!= error){ 
        處理數據 
        break; 
    } 
} 

     可是對於非阻塞IO就有一個很是嚴重的問題,在while循環中須要不斷地去詢問內核數據是否就緒,這樣會致使CPU佔用率很是高,所以通常狀況下不多使用while循環這種方式來讀取數據。

三、多路複用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模型來講,一旦事件響應體很大,那麼就會致使後續的事件遲遲得不處處理,而且會影響新的事件輪詢。

四、信號驅動IO模型

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

五、異步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個階段都會引發用戶線程阻塞,也就是內核進行數據拷貝的過程都會讓用戶線程阻塞。

6、兩種高性能IO設計模式

    在傳統的網絡服務設計模式中,有兩種比較經典的模式:一種是 多線程,一種是線程池。對於多線程模式,也就說來了client,服務器就會新建一個線程來處理該client的讀寫事件,以下圖所示:

 

     這種模式雖然處理起來簡單方便,可是因爲服務器爲每一個client的鏈接都採用一個線程去處理,使得資源佔用很是大。所以,當鏈接數量達到上限時,再有用戶請求鏈接,直接會致使資源瓶頸,嚴重的可能會直接致使服務器崩潰。

  所以,爲了解決這種一個線程對應一個客戶端模式帶來的問題,提出了採用線程池的方式,也就說建立一個固定大小的線程池,來一個客戶端,就從線程池取一個空閒線程來處理,當客戶端處理完讀寫操做以後,就交出對線程的佔用。所以這樣就避免爲每個客戶端都要建立線程帶來的資源浪費,使得線程能夠重用。

     可是線程池也有它的弊端,若是鏈接大可能是長鏈接,所以可能會致使在一段時間內,線程池中的線程都被佔用,那麼當再有用戶請求鏈接時,因爲沒有可用的空閒線程來處理,就會致使客戶端鏈接失敗,從而影響用戶體驗。所以,線程池比較適合大量的短鏈接應用。

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

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

      從這裏能夠看出,上面的五種IO模型中的多路複用IO就是採用Reactor模式。注意,上面的圖中展現的 是順序處理每一個事件,固然爲了提升事件處理速度,能夠經過多線程或者線程池的方式來處理事件。

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

相關文章
相關標籤/搜索