天天看點

Java IO:BIO和NIO差別及各自應用場景

轉載請注明出處:jiq•欽's technical Blog - 季義欽

引言

BIO和NIO是兩種不同的網絡通信模型,現如今NIO已經大量應用在Jetty、ZooKeeper、Netty等開源架構中。

一個面向流、一個面向緩沖區

一個是阻塞式的、一個非阻塞

一個沒有io多路複用器、一個有

下面通過一個例子解釋兩者差別:

假設目前服務端程式需要同時從與多個用戶端建立的連接配接讀取資料。

使用BIO

如果采用阻塞式IO,單線程情況下,處理者線程可能阻塞在其中一個套接字的read上,導緻另一個套接字即使準備好了資料也無法處理,這個時候解決的方法就是針對每一個套接字,都建立一個線程處理其資料讀取。

是以說,在BIO工作模式下,服務端程式要想同時處理多個套接字的資料讀取,在等待接收連接配接請求的主線程之外,還要為每一個建立好的連接配接配置設定一個新的線程進行處理。

使用NIO

輪詢方式

如果将套接字讀操作換成非阻塞的,那麼隻需要一個線程就可以同時處理套接字,每次檢查一個套接字,有資料則讀取,沒有則檢查下一個,因為是非阻塞的,是以執行read操作時若沒有資料準備好則立即傳回,不會發生阻塞。

I/O多路複用

這種輪詢的方式缺點是浪費CPU資源,大部分時間可能都是無資料可讀的,不必仍不間斷的反複執行read操作,I/O多路複用(IOmultiplexing)是一種更好的方法,調用select函數時,其内部會維護一張監聽的套接字的清單,其會一直阻塞直到其中某一個套接字有資料準備好才傳回,并告訴是哪個套接字可讀,這時再調用該套接字的read函數效率更高。

是以基本可以認為 “NIO = I/O多路複用 + 非阻塞式I/O”,大部分情況下是單線程,但也有超過一個線程實作NIO的情況

NIO三種模型

上面所講到的隻需要一個線程就可以同時處理多個套接字,這隻是其中的一種單線程模型,是一種較為極端的情況,NIO主要包含三種線程模型:

1) Reactor單線程模型

2) Reactor多線程模型

3)主從Reactor多線程模型

Reactor單線程模型:

單個線程完成所有事情包括接收用戶端的TCP連接配接請求,讀取和寫入套接字資料等。

對于一些小容量應用場景,可以使用單線程模型。但是對于高負載、大并發的應用卻不合适,主要原因如下:

1) 一個NIO線程同時處理成百上千的鍊路,性能上無法支撐,即便NIO線程的CPU負荷達到100%,也無法滿足海量消息的編碼、解碼、讀取和發送;

2) 當NIO線程負載過重之後,處理速度将變慢,這會導緻大量用戶端連接配接逾時,逾時之後往往會進行重發,這更加重了NIO線程的負載,最終會導緻大量消息積壓和處理逾時,NIO線程會成為系統的性能瓶頸;

3) 可靠性問題:一旦NIO線程意外跑飛,或者進入死循環,會導緻整個系統通信子產品不可用,不能接收和處理外部消息,造成節點故障。

為了解決這些問題,演進出了Reactor多線程模型。

Reactor多線程模型:

Rector多線程模型與單線程模型最大的差別就是有一組NIO線程處理真實的IO操作。

Reactor多線程模型的特點:

1) 有專門一個NIO線程-Acceptor線程用于監聽服務端,接收用戶端的TCP連接配接請求;

2) 網絡IO操作-讀、寫等由一個NIO線程池負責,線程池可以采用标準的JDK線程池實作,它包含一個任務隊列和N個可用的線程,由這些NIO線程負責消息的讀取、解碼、編碼和發送;

3) 1個NIO線程可以同時處理N條鍊路,但是1個鍊路隻對應1個NIO線程,防止發生并發操作問題。

在絕大多數場景下,Reactor多線程模型都可以滿足性能需求;但是,在極特殊應用場景中,一個NIO線程負責監聽和處理所有的用戶端連接配接可能會存在性能問題。例如百萬用戶端并發連接配接,或者服務端需要對用戶端的握手消息進行安全認證,認證本身非常損耗性能。在這類場景下,單獨一個Acceptor線程可能會存在性能不足問題,為了解決性能問題,産生了第三種Reactor線程模型-主從Reactor多線程模型。

即從單線程中由一個線程即監聽連接配接事件、讀寫事件、由完成資料讀寫,拆分為由一個線程專門監聽各種事件,再由專門的線程池負責處理真正的IO資料讀寫。

主從Reactor多線程模型

主從Reactor線程模型與Reactor多線程模型的最大差別就是有一組NIO線程處理連接配接、讀寫事件。

主從Reactor線程模型的特點是:服務端用于接收用戶端連接配接的不再是個1個單獨的NIO線程,而是一個獨立的NIO線程池。Acceptor接收到用戶端TCP連接配接請求處理完成後(可能包含接入認證等),将新建立的SocketChannel注冊到IO線程池(sub reactor線程池)的某個IO線程上,由它負責SocketChannel的讀寫和編解碼工作。Acceptor線程池僅僅隻用于用戶端的登陸、握手和安全認證,一旦鍊路建立成功,就将鍊路注冊到後端subReactor線程池的IO線程上,由IO線程負責後續的IO操作。

即從多線程模型中由一個線程來監聽連接配接事件和資料讀寫事件,拆分為一個線程監聽連接配接事件,線程池的多個線程監聽已經建立連接配接的套接字的資料讀寫事件,另外和多線程模型一樣有專門的線程池處理真正的IO操作。

各自适用場景

NIO适用場景

伺服器需要支援超大量的長時間連接配接。比如10000個連接配接以上,并且每個用戶端并不會頻繁地發送太多資料。例如總公司的一個中心伺服器需要收集全國便利店各個收銀機的交易資訊,隻需要少量線程按需處理維護的大量長期連接配接。

Jetty、Mina、Netty、ZooKeeper等都是基于NIO方式實作。

BIO适用場景

适用于連接配接數目比較小,并且一次發送大量資料的場景,這種方式對伺服器資源要求比較高,并發局限于應用中。