首頁  >  文章  >  Java  >  JAVA中I/O模型的詳細講解(附實例)

JAVA中I/O模型的詳細講解(附實例)

王林
王林轉載
2019-08-30 13:49:522120瀏覽

也許很多朋友在學習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段程式碼:

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

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

接著看下面這段程式碼:

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模型的理解非常重要。

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

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

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

三、什麼是阻塞I/O?什麼是非阻塞I/O?

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

四、什麼是同步I/O?什麼是非同步I/O?

我們先來看看同步IO與非同步IO的定義,在《Unix網路程式設計》一書中對同步IO與非同步IO的定義是這樣的:

  A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.<br>  An asynchronous I/O operation does nots to questing prolockcess to questing procesed 片# not not ca#wbe 完成. #  從字面的意思可以看出:同步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種I/O模型
#在《Unix網路程式設計》一書中提到了五種IO模型,分別是:阻塞IO、非阻塞IO、多工IO、訊號驅動IO、非同步IO。

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

1.阻塞IO模型

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

  当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。

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

data = socket.read();

如果数据没有就绪,就会一直阻塞在read方法。

2.非阻塞IO模型

  当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。

  所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。

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

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個階段都會引起使用者執行緒阻塞,也就是核心進行資料拷貝的過程都會讓使用者執行緒阻塞。

六、兩種高效能I/O設計模式

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

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

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

JAVA中I/O模型的詳細講解(附實例)

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

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

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

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

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

JAVA中I/O模型的詳細講解(附實例)

#從這裡可以看出,上面的五種IO模型中的多路復用IO就是採用Reactor模式。請注意,上面的圖中展示的 是順序處理每個事件,當然為了提高事件處理速度,可以透過多執行緒或執行緒池的方式來處理事件。

  在Proactor模式中,當偵測到有事件發生時,會新起一個非同步操作,然後交由核心執行緒去處理,當核心執行緒完成IO操作之後,發送一個通知告知操作已完成,可以得知,非同步IO模型採用的就是Proactor模式。

以上內容若有錯誤請多多諒解並歡迎您批評指正!

想了解更多相關內容請造訪PHP中文網:JAVA影片教學

以上是JAVA中I/O模型的詳細講解(附實例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除