如何理解 BIO、NIO、AIO 的區別?

來源:juejin.cn/post/6844903985158045703

很多文章在談論到 BIO、NIO、AIO 的時候僅僅是拋出一堆定義,以及一些生動的例子。看似很好理解。但是並沒有將最基礎的本質原理顯現出來,如果沒有沒有從 IO 的原理出發的話是很難理解這三者之間的區別的。所以本篇文章從 Java 是如何進行 IO 操作爲開頭進行分析。

Java 中的 IO 原理

首先 Java 中的 IO 都是依賴操作系統內核進行的,我們程序中的 IO 讀寫其實調用的是操作系統內核中的 read&write 兩大系統調用。

那內核是如何進行 IO 交互的呢?

  1. 網卡收到經過網線傳來的網絡數據,並將網絡數據寫到內存中。

  2. 當網卡把數據寫入到內存後,網卡向 cpu 發出一箇中斷信號,操作系統便能得知有新數據到來,再通過網卡中斷程序去處理數據。

  3. 將內存中的網絡數據寫入到對應 socket 的接收緩衝區中。

  4. 當接收緩衝區的數據寫好之後,應用程序開始進行數據處理。

對應抽象到 java 的 socket 代碼簡單示例如下:

public class SocketServer {
  public static void main(String[] args) throws Exception {
    // 監聽指定的端口
    int port = 8080;
    ServerSocket server = new ServerSocket(port);
    // server將一直等待連接的到來
    Socket socket = server.accept();
    // 建立好連接後,從socket中獲取輸入流,並建立緩衝區進行讀取
    InputStream inputStream = socket.getInputStream();
    byte[] bytes = new byte[1024];
    int len;
    while ((len = inputStream.read(bytes)) != -1) {
      //獲取數據進行處理
      String message = new String(bytes, 0, len,"UTF-8");
    }
    // socket、server,流關閉操作,省略不表
  }
}

可以看到這個過程和底層內核的網絡 IO 很類似,主要體現在 accept() 等待從網絡中的請求到來然後 bytes[] 數組作爲緩衝區等待數據填滿後進行處理。而 BIO、NIO、AIO 之間的區別就在於這些操作是同步還是異步,阻塞還是非阻塞。

所以我們引出同步異步,阻塞與非阻塞的概念。

同步與異步

同步和異步指的是一個執行流程中每個方法是否必須依賴前一個方法完成後纔可以繼續執行。假設我們的執行流程中:依次是方法一和方法二。

同步指的是調用一旦開始,調用者必須等到方法調用返回後,才能繼續後續的行爲。即方法二一定要等到方法一執行完成後纔可以執行。

異步指的是調用立刻返回,調用者不必等待方法內的代碼執行結束,就可以繼續後續的行爲。(具體方法內的代碼交由另外的線程執行完成後,可能會進行回調)。即執行方法一的時候,直接交給其他線程執行,不由主線程執行,也就不會阻塞主線程,所以方法二不必等到方法一完成即可開始執行。

同步與異步關注的是方法的執行方是主線程還是其他線程,主線程的話需要等待方法執行完成,其他線程的話無需等待立刻返回方法調用,主線程可以直接執行接下來的代碼。

同步與異步是從多個線程之間的協調來實現效率差異。

爲什麼需要異步呢?筆者認爲異步的本質就是爲了解決主線程的阻塞,所以網上很多討論把同步異步、阻塞非阻塞進行了四種組合,其中一種就有異步阻塞這一情形,如果異步也是阻塞的?那爲什麼要特地進行異步操作呢?

阻塞與非阻塞

阻塞與非阻塞指的是單個線程內遇到同步等待時,是否在原地不做任何操作。

阻塞指的是遇到同步等待後,一直在原地等待同步方法處理完成。

非阻塞指的是遇到同步等待,不在原地等待,先去做其他的操作,隔斷時間再來觀察同步方法是否完成。

阻塞與非阻塞關注的是線程是否在原地等待。

筆者認爲阻塞和非阻塞僅能與同步進行組合。而異步天然就是非阻塞的,而這個非阻塞是對主線程而言。(可能有人認爲異步方法裏面放入阻塞操作的話就是異步阻塞,但是思考一下,正是因爲是阻塞操作所以纔會將它放入異步方法中,不要阻塞主線程)

例子講解

海底撈很好喫,但是經常要排隊。我們就以生活中的這個例子進行講解。

哪種方式更有效率呢?是不是一目瞭然呢?

BIO

BIO 全稱是 Blocking IO,是 JDK1.4 之前的傳統 IO 模型,本身是同步阻塞模式。線程發起 IO 請求後,一直阻塞 IO,直到緩衝區數據就緒後,再進入下一步操作。針對網絡通信都是一請求一應答的方式,雖然簡化了上層的應用開發,但在性能和可靠性方面存在着巨大瓶頸,試想一下如果每個請求都需要新建一個線程來專門處理,那麼在高併發的場景下,機器資源很快就會被耗盡。

NIO

NIO 也叫 Non-Blocking IO 是同步非阻塞的 IO 模型。線程發起 io 請求後,立即返回(非阻塞 io)。同步指的是必須等待 IO 緩衝區內的數據就緒,而非阻塞指的是,用戶線程不原地等待 IO 緩衝區,可以先做一些其他操作,但是要定時輪詢檢查 IO 緩衝區數據是否就緒。

Java 中的 NIO 是 new IO 的意思。其實是 NIO 加上 IO 多路複用技術。普通的 NIO 是線程輪詢查看一個 IO 緩衝區是否就緒,而 Java 中的 new IO 指的是線程輪詢地去查看一堆 IO 緩衝區中哪些就緒,這是一種 IO 多路複用的思想。IO 多路複用模型中,將檢查 IO 數據是否就緒的任務,交給系統級別的 select 或 epoll 模型,由系統進行監控,減輕用戶線程負擔。

NIO 主要有 buffer、channel、selector 三種技術的整合,通過零拷貝的 buffer 取得數據,每一個客戶端通過 channel 在 selector(多路複用器)上進行註冊。服務端不斷輪詢 channel 來獲取客戶端的信息。channel 上有 connect,accept(阻塞)、read(可讀)、write(可寫) 四種狀態標識。根據標識來進行後續操作。所以一個服務端可接收無限多的 channel。不需要新開一個線程。大大提升了性能。

AIO

AIO 是真正意義上的異步非阻塞 IO 模型。上述 NIO 實現中,需要用戶線程定時輪詢,去檢查 IO 緩衝區數據是否就緒,佔用應用程序線程資源,其實輪詢相當於還是阻塞的,並非真正解放當前線程,因爲它還是需要去查詢哪些 IO 就緒。而真正的理想的異步非阻塞 IO 應該讓內核系統完成,用戶線程只需要告訴內核,當緩衝區就緒後,通知我或者執行我交給你的回調函數。

AIO 可以做到真正的異步的操作,但實現起來比較複雜,支持純異步 IO 的操作系統非常少,目前也就 windows 是 IOCP 技術實現了,而在 Linux 上,底層還是是使用的 epoll 實現的。

筆者個人理解總結,如有錯誤懇請網友評論指正。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rfqb_Ee_Gw1WPAGgN8EYGg