從 BIO 到 NIO,再到 AIO

BIO 模型,每當有一個連接到來,經(jīng)過協(xié)調(diào)器的處理,就開啟一個對應的線程進行接管。如果連接有 1000 條,那就需要 1000 個線程。
線程資源是非常昂貴的,除了占用大量的內(nèi)存,還會占用非常多的 CPU 調(diào)度時間,所以 BIO 在連接非常多的情況下,效率會變得非常低。
為什么 NIO 的性能就能夠比傳統(tǒng)的阻塞 I/O 性能高呢?
Java 的 NIO,在 Linux 上底層是使用 epoll 實現(xiàn)的。epoll 是一個高性能的多路復用 I/O 工具,改進了 select 和 poll 等工具的一些功能
epoll 的數(shù)據(jù)結(jié)構(gòu)是直接在內(nèi)核上進行支持的,通過 epoll_create 和 epoll_ctl 等函數(shù)的操作,可以構(gòu)造描述符(fd)相關(guān)的事件組合(event)。
fd 每條連接、每個文件,都對應著一個描述符,比如端口號。內(nèi)核在定位到這些連接的時候,就是通過 fd 進行尋址的。
event 當 fd 對應的資源,有狀態(tài)或者數(shù)據(jù)變動,就會更新 epoll_item 結(jié)構(gòu)。在沒有事件變更的時候,epoll 就阻塞等待,也不會占用系統(tǒng)資源;一旦有新的事件到來,epoll 就會被激活,將事件通知到應用方。
BIO 的讀寫操作是阻塞的,線程的整個生命周期和連接的生命周期是一樣的,而且不能夠被復用。
當服務的連接增多,考慮到整個服務器的資源調(diào)度和資源利用率等因素,NIO 就有了顯著的效果,NIO 非常適合高并發(fā)場景。

接下來,在 while 循環(huán)里,使用 select 函數(shù),阻塞在主線程里。所謂阻塞,就是操作系統(tǒng)不再分配 CPU 時間片到當前線程中,所以 select 函數(shù)是幾乎不占用任何系統(tǒng)資源的。
一旦有新的事件到達,比如有新的連接到來,主線程就能夠被調(diào)度到,程序就能夠向下執(zhí)行。這時候,就能夠根據(jù)訂閱的事件通知,持續(xù)獲取訂閱的事件。
由于注冊到 selector 的連接和事件可能會有多個,所以這些事件也會有多個。我們使用安全的迭代器循環(huán)進行處理,在處理完畢之后,將它刪除。
關(guān)于 epoll 還會有一個面試題,相對于 select,epoll 有哪些改進?
epoll 不再需要像 select 一樣對 fd 集合進行輪詢,也不需要在調(diào)用時將 fd 集合在用戶態(tài)和內(nèi)核態(tài)進行交換;
應用程序獲得就緒 fd 的事件復雜度,epoll 是 O(1),select 是 O(n);
select 最大支持約 1024 個 fd,epoll 支持 65535個;
select 使用輪詢模式檢測就緒事件,epoll 采用通知方式,更加高效。
如果事件不刪除的話,或者漏掉了某個事件的處理,會有什么后果?
由于每次判斷都會有事件,就會造成select線程的頻繁喚醒,進而造成CPU的使用飆升。
op_write
這個事件是表示寫就緒的,當?shù)讓拥木彌_區(qū)有空閑,這個事件就會一直發(fā)生,浪費占用 CPU 資源。所以,我們一般是不注冊 OP_WRITE 的。
為什么我在使用 NIO 時,使用 Channel 進行讀寫,socket 的操作依然是阻塞的?NIO 的作用主要體現(xiàn)在哪里?
NIO 只負責對發(fā)生在 fd 描述符上的事件進行通知。事件的獲取和通知部分是非阻塞的,但收到通知之后的操作,卻是阻塞的,即使使用多線程去處理這些事件,它依然是阻塞的。
AIO 更近一步,將這些對事件的操作也變成非阻塞的
AIO 是 Java 1.7 加入的,理論上性能會有提升,但實際測試并不理想。這是因為,AIO主要處理對數(shù)據(jù)的自動讀寫操作。這些操作的具體邏輯,假如不放在框架中,也要放在內(nèi)核中,并沒有節(jié)省操作步驟,對性能的影響有限。而 Netty 的 NIO 模型加上多線程處理,在這方面已經(jīng)做得很好,編程模式也比AIO簡單。
所以,市面上對 AIO 的實踐并不多,在采用技術(shù)選型的時候,一定要謹慎

