1. 程式人生 > 實用技巧 >Java IO模型知識梳理

Java IO模型知識梳理

(本文大部分內容非原創,是自己整理複習的知識點。在最下面都會給上所有知識點的來源參考或出處,需要深入瞭解可以通過連結跳轉)

概述

IO模型可以理解為用什麼樣的通道進行資料的傳送和接收,很大程度上決定了程式通訊的效能。而在我們Java中支援了3種的IO網路模型,分別是BIO、NIO、AIO。

這三種模型呢可以理解為是Java語言對作業系統的各種IO模型(五大IO模型)的封裝。程式設計師在使用這些API的時候,不需要關心作業系統層面的知識,也不需要根據不同作業系統編寫不同的程式碼。而在我們學習這種三種IO的時候一定要先了解這幾個概念:同步與非同步,阻塞和非阻塞。

同步與非同步

同步與非同步可以理解為一種通訊機制,指應用程式與核心的互動而言。

同步:同步可以理解為使用者發起一個請求呼叫之後,在請求完成之前,呼叫不返回使用者也不執行。

非同步:非同步可以理解為使用者發起一個請求呼叫之後,在請求完成之前 ,使用者可以繼續執行;當呼叫完成之後會通知使用者,或者呼叫使用者的回撥函式。

阻塞與非阻塞

阻塞和非阻塞可以理解為一種呼叫狀態,指的是程序在訪問資料的時候採用的方式。

阻塞: 阻塞就是發起一個請求,呼叫者一直等待請求結果返回,也就是當前執行緒會被掛起,無法從事其他任務,只有當條件就緒才能繼續。

非阻塞: 非阻塞就是發起一個請求,呼叫者不用一直等著結果返回,可以先去幹其他事情。

概念區分

同步/非同步是從行為角度描述事物的,而阻塞和非阻塞描述的當前事物的狀態(等待呼叫結果時的狀態)。

BIO(Blocking I/O)

同步阻塞IO,通常由一個獨立的Acceptor執行緒負責監聽客戶端的連線,它接收到客戶端連結請求之後為每一個客戶端建立一個新的執行緒進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端,執行緒銷燬。這是典型的一請求一應答通訊模型。

但是執行緒是寶貴的資源,而且建立和銷燬以及切換的成本都很高。所以如果使用這種一對一應答通訊模型,哪怕這個連結不做任何事情的話也會造成不必要的執行緒開銷。可以想到如果在客戶端併發量大量增加後這種模型就會出現因為執行緒數量急劇膨脹可能會導致執行緒堆疊溢位、建立新執行緒失敗等問題,最終導致程序宕機或者殭屍,不能對外提供服務。

偽非同步 IO

但是這種情況是可以改善的,為了解決同步阻塞IO面臨的一個鏈路需要一個執行緒處理的問題,後來有人對它的執行緒模型進行了優化,後端通過一個執行緒池來處理多個客戶端的請求接入,形成客戶端個數M:執行緒池最大執行緒數N的比例關係,其中M可以遠遠大於N,通過執行緒池可以靈活的調配執行緒資源,設定執行緒的最大值,防止由於海量併發接入導致執行緒耗盡。

它的過程主要是:客戶端接入的時候,將客戶端的Socket封裝成一個Task然後投遞到後端的執行緒池進行處理,JDK維護一個訊息佇列和N個活躍執行緒對訊息佇列中的任務進行處理。

總結

偽非同步IO通訊框架採用了執行緒池實現,因此避免了為麼一個請求都建立一個獨立執行緒造成的執行緒資源耗盡問題。但是由於它底層的通訊依然採用同步阻塞模型,因此無法從根本上解決問題。

適用場景

適用於連線數量比較小且固定的架構,而且伺服器資源多。這是JDK1.4以前的唯一選擇,但是程式簡單容易理解。

NIO

NIO(Non-blocking I/O,在Java領域,也稱為New I/O),是一種同步非阻塞的I/O模型,也是I/O多路複用的基礎,已經被越來越多地應用到大型應用伺服器,成為解決高併發與大量連線、I/O處理問題的有效方式。

同步是指執行緒還是要不斷接收客戶端連線並處理資料,非阻塞是指如果一個管道沒有資料,不需要等待,可以輪詢下一個管道。

NIO是支援面向緩衝的,基於通道的I/O操作方法。 它提供了與傳統BIO模型中的 SocketServerSocket 相對應的 SocketChannelServerSocketChannel 兩種不同的套接字通道實現,兩種通道都支援阻塞和非阻塞兩種模式。阻塞模式使用就像傳統中的支援一樣,比較簡單,但是效能和可靠性都不好;非阻塞模式正好與之相反。對於低負載、低併發的應用程式,可以使用同步阻塞I/O來提升開發速率和更好的維護性;對於高負載、高併發的(網路)應用,應使用 NIO 的非阻塞模式來開發。

I/O 與 NIO 最重要的區別是資料打包和傳輸的方式,I/O 以流的方式處理資料,而 NIO 以塊的方式處理資料。NIO的主要原理是伺服器實現模式為多個連線請求對應一個執行緒,客戶端連線請求會註冊到一個多路複用器 Selector ,Selector 輪詢到連線有 IO 請求時才啟動一個執行緒處理。

如果要詳細一點介紹NIO的話,就可以從它的特性和三大核心元件開始介紹。

Buffer(緩衝區)

我們原來的IO是面向流(Stream),而NIO是面向Buffer(緩衝區)。Buffer是一個物件,它包含一些寫入或者要讀出的資料。在NIO類庫中加入Buffer物件,體現了新庫與原I/O的一個重要區別。在面向流的I/O中·可以將資料直接寫入或者將資料直接讀到 Stream 物件中。雖然 Stream 中也有 Buffer 開頭的擴充套件類,但只是流的包裝類,還是從流讀到緩衝區,而 NIO 卻是直接讀到 Buffer 中進行操作。

在NIO中,所有的資料讀寫操作都是通過這個緩衝區來完成的,而我們的緩衝區實質上就是一個數組。通常它是一個位元組陣列(ByteBuffer),同時也可以使用其他種類的陣列。但它又不僅僅只是陣列,它也提供了對資料的結構化訪問以及維護讀寫位置等資訊。

最常用的緩衝區是ByteBuffer,而Java基本型別除了Boolean都有對應的緩衝區,這裡就不描述了。這些Buffer型別的類其實都是Buffer介面的一個子例項。它們都有完全一樣的操作,只是處理的資料型別不一樣而已。

Buffer的讀寫主要屬性方法

Buffer有三個比較重要的屬性,用來操作過程讀寫過程的位置。

  1. position:下次讀寫資料的位置
  2. limit:本次讀寫的極限位置
  3. capacity:最大容量

在我們的讀寫過程主要也有這三個比較重要的方法。

  1. flip :將寫轉為讀,底層實現原理把 position 置 0,並把 limit 設為當前的 position 值。

  2. clear :將讀轉為寫模式(用於讀完全部資料的情況,把 position 置 0,limit 設為 capacity)。

  3. compact:將讀轉為寫模式(用於存在未讀資料的情況,讓 position 指向未讀資料的下一個)。

Buffer的讀寫過程舉例

① 新建一個大小為 8 個位元組的緩衝區,此時 position 為 0,而 limit = capacity = 8。capacity 變數不會改變,下面的討論會忽略它。

② 從輸入通道中讀取 5 個位元組資料寫入緩衝區中,此時 position 為 5,limit 保持不變。

③ 在將緩衝區的資料寫到輸出通道之前,需要先呼叫 flip() 方法,這個方法將 limit 設定為當前 position,並將 position 設定為 0。

④ 從緩衝區中取 4 個位元組到輸出緩衝中,此時 position 設為 4。

⑤ 最後需要呼叫 clear() 或者compact()方法來清空緩衝區,此時 position 和 limit 都被設定為最初位置。

⑥ 因為通道方向和 Buffer 方向相反,所以讀資料相當於向 Buffer 寫,寫資料相當於從 Buffer 讀。

Channel(管道)

雙向通道,替換了 BIO 中的 Stream 流,不能直接訪問資料,要通過 Buffer 來讀寫資料,也可以和其他 Channel 互動。同時也因為Channel是全雙工的,所以它可以比流更好地對映底層作業系統的API。特別是在Unix網路程式設計模型(五大IO模型)中,底層作業系統的通道全是全雙工的,同時支援讀寫操作。

管道主要實現了頂級介面Channel,然後下面有很多擴充套件了的Channel,一般通用的包括以下型別

  • FileChannel:從檔案中讀寫資料;
  • DatagramChannel:通過 UDP 讀寫網路中資料;
  • SocketChannel:通過 TCP 讀寫網路中資料;
  • ServerSocketChannel:可以監聽新進來的 TCP 連線,對每一個新進來的連線都會建立一個 SocketChannel。

Selector(多路複用器)

NIO 實現了 IO 多路複用中的 Reactor 模型,即一個執行緒 Thread 使用一個選擇器 Selector 通過輪詢的方式去監聽多個通道 Channel 上的事件,從而讓一個執行緒就可以處理多個事件。

具體過程就是:

輪詢檢查多個 Channel 的狀態,判斷註冊事件是否發生,即判斷 Channel 是否處於可讀或可寫狀態。使用前需要將 Channel 註冊到 Selector,註冊後會得到一個 SelectionKey,通過 SelectionKey 獲取 Channel 和 Selector 相關資訊。

我們在上面說過NIO是同步非阻塞的,所以當我們的Channel上的IO事件還沒到達時,不會進入阻塞狀態一直等待,而是繼續輪詢其他Channel,找到IO事件已經到達的Channel來執行。

同時需要注意的是隻有套接字Channel(TCP,UDP)才能配置為非阻塞,而FileChannel不能,因為檔案設定成了非阻塞也不會提升效率,檔案總是可讀可寫的(就緒狀態)。我們的非阻塞可以去輪詢是否可以有資料可以寫,如果沒有的話可以繼續輪詢而不必要去等待減少阻塞時間,所以比較適用於網路IO。但是磁碟IO裡的檔案總是可以讀寫的,所以非阻塞沒有什麼意思。

NIO適合應用場景

適用於連線數目多且連線比較長輕操作)的架構,比如聊天伺服器、彈幕伺服器、伺服器間通訊等,程式設計比較複雜,JDK1.4開始支援。

AIO

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改進版 NIO 2,它是非同步非阻塞的IO模型。

非同步IO的伺服器實現模式為一個有效請求對應一個執行緒,客戶端的 IO 請求都是由作業系統先完成 IO 操作後再通知伺服器應用來直接使用準備好的資料。這裡的非同步是指服務端執行緒接收到客戶端管道後就交給底層處理IO通訊,自己可以做其他事情,非阻塞是指客戶端有資料才會處理,處理好再通知伺服器。

AIO模型在netty中未使用,也沒有得到廣泛的運用,所以也不用過多介紹了。

AIO適合應用場景

AIO適用於連線數目多且連線比較長重操作)的架構,比如相簿伺服器,充分呼叫OS參與併發操作,程式設計比較複雜,JDK7開始支援。

參考資料

《Netty權威指南》

Java NIO淺析——《美團技術團隊》

BIO,NIO,AIO總結——《JavaGuide》

Java IO——《cyc2018》

Java IO模型之BIO、NIO、AIO三大IO模型