1. 程式人生 > >I/O 模型與 Java

I/O 模型與 Java

本文主要記錄 Java 中 I/O 模型及相關知識點 。另,由於自身知識水平有限,如有不正之處,望各位能夠諒解,歡迎批評指正!

一、I/O 基礎

  由於 Java 中 I/O 相關的 API ,無論是 BIO 還是 NIO,都是相對抽象的。所以我先整理一下 I/O 的基礎知識,瞭解一下作業系統是如何處理 I/O 操作的。這一部分的內容,主要來自於『Java NIO』一書,中譯版的 1.4 節:I/O 概念。

1.1 緩衝區操作

  緩衝區,以及緩衝區如何工作,是所有 I/O 的基礎。I/O 操作也就是 input/output,輸入/輸出操作,實際上就是將資料移進/移出緩衝區。程序執行 I/O 操作,歸結起來,也就是向作業系統發出請求,讓它將緩衝區排幹(寫),或者用資料將緩衝區填滿(讀)。這裡面的讀可能好理解一點,寫估計就有點不太直白了。個人理解:就是將緩衝區之前寫入的資料移出去,也就是傳送,感覺和 Java 中 I/O 操作的 flush() 方法有點類似的感覺。

  下圖簡單描述了資料從外部磁碟向執行中的程序的記憶體區域移動的過程。程序使用 read() 系統呼叫,要求其緩衝區被填滿。核心隨即向磁碟控制硬體發出命令,要求其從磁碟讀取資料。磁碟控制器把資料直接寫入核心記憶體緩衝區,當磁碟控 制器把緩衝區裝滿,核心即把資料從核心空間的臨時緩衝區拷貝到程序執行 read() 呼叫時指定的緩衝區。 

  注意,圖中使用者空間和核心空間的概念。使用者空間是常規程序所在區域是非特權區域:比如,在該區域執行的程式碼不能直接訪問硬體裝置。核心空間是作業系統所在區域是特權區域:它能與裝置控制器通訊,控制著使用者區域 程序的執行狀態,等等。最重要的是,所有 I/O 都直接或間接通過核心空間

  為什麼 I/O 操作都要直接或間接的經過核心空間?我能想到的有兩點:一是安全,二是效率。書中也介紹了分散/匯聚高效的處理資料原因。

二、BIO 和 NIO

在前面演示的 I/O 過程中,大致分為兩個步驟:

  1. 使用者空間的程序發起 I/O 請求,作業系統接收到請求後,系統核心將資料從硬體中讀取到核心緩衝區(準備需要的資料);
  2. 核心空間的系統核心將資料從核心緩衝區移動到使用者空間的緩衝區,然後資料可供使用者程序使用(填充緩衝區)。

  顯然,這兩個步驟或多或少都會消耗一定的時間。那麼 BIO 和 NIO 的區別就在這兩個步驟中區別開了。

2.1 BIO

  IO 稱之為阻塞 I/O。在第一個階段,使用者程序發起 I/O 請求後,程式會處於阻塞狀態,直到核心將資料準備好。然後,在第二個階段,使用者程序仍然處於阻塞狀態,直到核心完成使用者空間緩衝區的填充,最後使用者程序開始使用資料。也就是說使用者程序從發起請求的那一刻,直到資料可用,一直都處於阻塞狀態。

2.2 NIO

  NIO 稱之為非阻塞 I/O。在第一個階段,使用者程序發起 I/O 請求後,如果核心未將資料準備好,請求會直接返回,並表明資料沒有準備好。此時使用者程序不會繼續等待,會繼續執行。一般我們都是重複的去檢查核心是否將資料準備好。當資料準備好之後,進入第二階段,發起填充緩衝區的請求,此時即便是 NIO 也會阻塞到使用者空間的緩衝區填充完成。

  由此,我們可以發現,BIO 和 NIO 的區別是在 I/O 操作的第一個階段,而第二個階段,兩者都會阻塞

  在這裡,我曾經有過這樣的誤會:既然 NIO 也是迴圈的去檢查核心是否將資料準備好,程式本身就是要去處理即將獲取的資料,那麼它非阻塞的意義在哪兒呢,還不如直接阻塞的等待,何必浪費 CPU 不停的輪詢呢?緊接著我看了多路複用 I/O,才算明白了 NIO 的優勢。

三、多路複用 I/O

  實際上 Java 中的 NIO 是多路複用 I/O 模型,真正的優勢是其執行緒模型。而實現這個執行緒模型的關鍵在於其使用的系統呼叫是 select()。在介紹 select()呼叫前,先宣告一點:Java 中的 NIO 非阻塞的特性僅適用於網路 I/O所以下面主要針對網路 I/O 來講

  select() 會返回當前所有資料準備就緒的 Socket,如果當前沒有資料準備就緒的 Socket,select() 就會阻塞。那麼只需要一個執行緒就能處理多個 Socket 的資料。回顧一下 BIO,當我們建立一個 Socket 連線時,這個建立連線的執行緒就會阻塞,直到有資料可用,也就是一個執行緒必須對應一個 Socket。

  BIO 的劣勢在於單機中執行緒數量是有限的,我覺得肯定比 Socket 連線數少很多吧。其次當執行緒數較多時,執行緒之間的切換會消耗掉很多的系統資源。並且當大部分的 Socket 連線的資料傳輸並不是很密集的時候,這些 Socket 對應的執行緒都處於阻塞狀態,這顯然是空佔記憶體!此時就是使用 Java NIO 的絕佳機會。

  可能說了那麼多,大家都覺得 BIO 一無是處,Java 網路程式設計直接上 NIO 就 Okay!其實我覺得兩者都有自己適用的環境,沒有最好的,只有更合適的。

  1. BIO 適用場景:
    普通的檔案讀寫、Socket 連線數量較少、資料傳輸密集等場景。在網路程式設計中,一般用於客戶端。
  2. NIO 適用場景:
    普通的檔案讀寫、Socket 連線數較多、需要考慮高併發等場景。在網路程式設計中,一般用於服務端。

四、AIO

AIO 稱之為非同步 I/O。AIO 是最理想的 I/O 處理方式。在 I/O 操作的兩個階段均不阻塞,當用戶程式發起 I/O 請求時直接返回。然後系統核心完成接下的所有操作之後,會通知使用者程式直接使用資料。

由於第二個階段是由系統核心主動完成的,所以 AIO 需要作業系統底層的支援。

五、總結

 下面是我在網上扒來的一張圖,很好的說明了各類 I/O 模型。

 關於同步與非同步、阻塞與非阻塞,文末給出的參考資料值得一看。

參考資料