1.什么是IO
雖然作為Java開發程序員,很多都聽過IO、NIO這些,但是很多人都沒深入去了解這些內容。
- Java的I/O是以流的方式進行數據輸入輸出的,Java的類庫涉及很多領域的IO內容:標準的輸入輸出,文件的操作、網絡上的數據傳輸流、字符串流、對象流等
2.同步與異步、阻塞與非阻塞
- 同步:一個任務完成之前不能做其他操作,必須等待。
- 異步:一個任務完成之前,可以進行其他操作
- 阻塞:相對于CPU來說,掛起當前線程,不能做其他操作只能等待
- 非阻塞:CPU無需掛起當前線程,可以執行其他操作
3.三種IO模型
BIO(Blocking I/O)
同步并阻塞模式,調用方在發起IO操作時會被阻塞,直到操作完成才能繼續執行,適用于連接數較少的場景。
例如:服務端通過ServerSocket監聽端口,accept()阻塞等待客戶端連接。
優缺點:
- 優點:實現簡單
- 缺點:線程資源開銷大,連接數多時,每個線程都要占用CPU資源,容易出現性能瓶頸
適用于低并發、短連接的場景,如傳統的HTTP服務

NIO(Non-blocking I/O)
同步非阻塞模型,客戶端發送的連接請求都會注冊到Selector多路復用器上,服務器端通過Selector管理多個通道Channel,Selector會輪詢這些連接,當輪詢到連接上有IO活動就進行處理。
NIO基于 Channel 和 Buffer 進行操作,數據總是從通道讀取到緩沖區或者從緩沖區寫入到通道。Selector 用于監聽多個通道上的事件(比如收到連接請求、數據達到等等),因此使用單個線程就可以監聽多個客戶端通道。
IO多路復用:一個線程可對應多個連接,不用為每個連接都創建一個線程

核心組件:
- Channel:雙向通信通道(如SocketChannel),數據可流入流出
- Buffer:數據緩沖區,是雙向的,可讀可寫
- Selector:一個Selector對應一個線程,一個Selector上可注冊多個Channel,并輪詢多個Channel的就緒事件
優缺點:
- 可以減少線程數量,降低線程切換的開銷,適用于需要處理大量并發連接的場景
- 缺點:實現復雜度高
使用于高并發、長連接的場景,如即時通訊場景
AIO(Asynchronous I/O)
異步非阻塞模型,基于事件回調或Future機制
- 調用方發起IO請求后,無需等待操作完成,可繼續執行其他任務。操作系統在IO操作完成后,通過回調或事件通知的方式告知調用方
- Java中
AsynchronousSocketChannel是AIO的代表類,通過回調函數處理讀寫操作完成后的結果
優缺點:
- IO密集型的應用,AIO提供更高的并發和低延遲,因為調用方在等待IO時不會被阻塞
- 缺點:實現復雜
適用于高吞吐、低延遲的場景,如日志批量寫入
4.什么是Netty
說起Java的IO模型,繞不開的就是Netty框架了,那什么是Netty,為什么Netty的性能這么高呢?
- Netty是由JBOSS提供的一個Java開源框架。提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器
- Netty的原理就是NIO,是基于NIO的完美封裝
很多中間件的底層通信框架用的都是它,比如:RocketMQ、Dubbo、Elasticsearch
4.1 Netty的核心要點
核心特點:
- 高并發:通過多路復用Selector實現單線程管理大量連接,減少線程開銷
- 傳輸快:零拷貝技術,減少內存拷貝次數
- 封裝性:簡化NIO的復雜API,提供鏈式處理(ChannelPipeline)和可擴展的編解碼能力(如Protobuf支持)
高性能的核心原因:
- 主從Reactor線程模型,無鎖化設計,減少線程競爭
- 零拷貝技術,堆外內存直接操作
- 高效內存管理,對象池技術,預分配內存塊并復用,對象復用機制
- 基于Selector的I/O多路復用,異步事件驅動機制
- Selector空輪詢問題修復
4.2 零拷貝技術
Netty的零拷貝體現在操作數據時, 不需要將數據 buffer從 一個內存區域拷貝到另一個內存區域。少了一次內存的拷貝,CPU 效率就得到的提升。
4.2.1 Linux系統的文件從本地磁盤發送到網絡中的零拷貝技術

- 內核緩沖區是 Linux 系統的 Page Cache。為了加快磁盤的 IO,Linux 系統會把磁盤上的數據以 Page 為單位緩存在操作系統的內存里
- 內核緩沖區到 Socket 緩沖區之間并沒有做數據的拷貝,只是一個地址的映射,底層的網卡驅動程序要讀取數據并發送到網絡上的時候,看似讀取的是 Socket 的緩沖區中的數據,其實直接讀的是內核緩沖區中的數據。
- 零拷貝中所謂的“零”指的是內存中數據拷貝的次數為 0
4.2.2 Netty零拷貝技術
- 使用了堆外內存進行Socket讀寫,避免JVM堆內存到堆外內存的數據拷貝
- 提供了CompositeByteBuf合并對象,可以組合多個Buffer對象合并成一個邏輯上的對象,用戶可以像操作一個Buffer那樣對組合Buffer進行操作,避免傳統內存拷貝合并
- 文件傳輸使用FileRegion,封裝FileChannel#transferTo()方法,將文件緩沖區的內容直接傳輸到目標Channel,避免內核緩沖區和用戶態緩沖區間的數據拷貝
4.2.3 Netty和操作系統的零拷貝的區別?
Netty 的 Zero-copy 完全是在用戶態(Java 應用層)的, 更多的偏向于優化數據操作。而在 OS 層面上的 Zero-copy 通常指避免在用戶態(User-space)與內核態(Kernel-space)之間來回拷貝數據
4.3 Reactor模式

- 基于IO多路復用技術,多個連接共用一個多路復用器,程序只需要阻塞等待多路復用器即可
- 基于線程池技術復用線程資源,程序將連接上的任務分配給線程池中線程處理,不用為每個連接單獨創建線程
- Reactor是圖中的ServiceHandler,在一個單獨線程中運行,負責監聽和分發事件
Reactor可以分為單Reactor單線程模式、單Reactor多線程模型,主從Reactor多線程模型
4.3.1 單Reactor單線程模式

- Reactor通過select監聽客戶端請求事件,收到事件后通過dispatch分發
該模式簡單,所有操作都由1個IO線程處理,缺點是存在性能瓶頸,只有1個線程工作,無法發揮多核CPU的性能。
4.3.2 單Reactor多線程模式

- Reactor主線程負責接收建立連接事件和后續的IO處理,Worker線程池處理具體業務邏輯
充分發揮了多核CPU的處理能力,缺點是用一個線程接收事件和響應,高并發時仍然會有性能瓶頸
4.3.3 主從Reactor多線程模式

- Reactor主線程負責通過select監聽連接事件,通過acceptor處理連接事件
- Reactor從線程負責處理建立連接后的IO處理事件
- worker線程池負責業務邏輯處理,并將結果返回給Handler
該模式優點是主從線程分工明確,能應對更高的并發。缺點是編程復雜度較高。
應用該模式的中間件有:Dubbo、RocketMQ、Zookeeper等
小結
Reactor模式的核心在于用一個或少量線程來監聽多個連接上的事件,根據事件類型分發調用相應處理邏輯,從而避免為每個連接都分配一個線程
4.4 Netty的線程模型

- BossGroup:boss線程組,負責接收客戶端的連接請求,連接來了之后,將其注冊到Worker線程組的NioEventLoop中
- WorkerGroup:Worker線程組,每個線程都是一個NioEventLoop,負責和處理一個或多個Channel的I/O讀寫操作。處理邏輯通常是通過ChannelPipeline中的各個ChannelHandler來完成
- 業務線程組(可選):還可以引入一個業務線程組來處理業務邏輯,避免阻塞Worker線程
簡單理解:Boss線程是老板,Worker線程是員工,老板負責接收處理的事件請求,Worker負責工作,處理請求的I/O事件,并交給對應的Handler處理
本質是將線程連接和具體的業務處理分開
5.多路復用I/O的3種機制
5.1 select
這三種都是操作系統中的多路復用I/O機制
輪詢機制:select使用一個固定大小的位圖來表示文件描述符集,將文件描述符的狀態(如可讀、可寫)存儲在一個數組中,調用select時,每次需將完整的位圖從用戶空間拷貝到內核空間,內核遍歷所有描述符,檢查就緒狀態
局限:
- 文件描述符限制通常為1024,限制了并發處理數
- 性能低:搞并發場景,每次都要遍歷整個位圖,性能開銷大,時間負責度為O(N)
5.2 poll
poll使用了動態數組來替代位圖,使用pollfd結構數組存儲文件描述符和事件,無數量限制
工作機制:每次調用時仍然需要遍歷所有描述符,即使只有少量描述符修改了,仍然要檢查整個數組,時間復雜度為O(N)
5.3 epoll
1)事件驅動模型:epoll使用紅黑樹來存儲和管理注冊的文件描述符,使用就緒事件鏈表來存儲觸發的事件。當某個文件描述符上的事件就緒時,epoll會將該文件描述符添加到就緒鏈表中。
2)觸發模式:支持水平觸發(LT)和邊緣觸發(ET),ET模式下事件僅通知一次
- 水平觸發(Level Triggered),默認模式,只要文件描述符上有未處理的數據,每次調用epoll_wait都會返回該文件描述符
- 邊緣觸發(Edge Triggered),僅在狀態發生變化時通知一次,減少重復事件的通知次數
3)工作流程:
epoll_create創建實例:分配相應數據結構,并返回一個epoll文件描述符。內核分配一棵紅黑樹管理文件描述符,以及一個就緒事件的鏈表epoll_ctl注冊、修改、刪除事件:epoll_ctl是用于管理文件描述符與事件關系的接口epoll_wait等待事件:epoll會檢查就緒事件鏈表,將鏈表中所有就緒的文件描述符返回給用戶空間。epoll_wait高效體現在它返回的是已經發生事件的文件描述符,而不是遍歷所有注冊的文件描述符
優點是時間復雜度O(1),僅處理活躍連接,性能和連接數無關
4)零拷貝機制:
- 通過內存映射mmap減少了在內核和用戶空間之間的數據復制,進一步提高了性能
總結:epoll每次只傳遞發生的事件,不需要傳遞所有文件描述符,所以提高了效率
6. Netty如何解決JDK NIO空輪詢bug的?
Java NIO在Linux系統下默認是epoll機制,理論上無客戶端連接時Selector.select()方法是會阻塞的。
發生空輪詢bug表現時,即時select輪詢事件返回數量是0,Select.select()方法也不會被阻塞,NIO就會一直處于while死循環中,不斷向CPU申請資源導致CPU 100%
底層原因:
- Linux內核在某些情況下會錯誤地將Selector的EPOLLUP(連接掛起)和EPOLLERR(錯誤)事件標記為就緒狀態,JDK中的NIO實現未正確處理這些事件,導致select()方法誤判事件存在而提前返回
6.1 Netty的解決方式
Netty并沒有解決這個bug,而是繞開了這個錯誤,具體如下:
1)統計空輪詢次數:通過selectCnt計數器來統計連續空輪詢的次數,每次執行Selector.select()方法后,如果發現沒有IO事件,selectCnt就會遞增
2)設置閾值:定義了一個閾值,默認為512,當空輪詢達到這個閾值時,Netty就會觸發重建Selector的操作
3)重建Selector:Netty新建一個Selector,并將所有注冊的Channel從舊的Selector轉移到新的Selector上,過程涉及取消舊Selector上的注冊,以及新Selector上重新注冊
4)關閉舊的Selector:重建Selector并將Channel重新注冊后,Netty關閉舊的Selector
總結:通過SelectCnt統計沒有IO事件的次數,來判斷當前是否發生了空輪詢,如果發生了,就重建一個Selector來替換之前出問題的Selector
核心代碼如下:
long time = System.nanoTime();
//調用select方法,阻塞時間為上面算出的最近一個將要超時的定時任務時間
int selectedKeys = selector.select(timeoutMillis);
//計數器加1
++selectCnt;
if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
//進入這個分支,表示正常場景
//selectedKeys != 0: selectedKeys個數不為0, 有io事件發生
//oldWakenUp:表示進來時,已經有其他地方對selector進行了喚醒操作
//wakenUp.get():也表示selector被喚醒
//hasTasks() || hasScheduledTasks():表示有任務或定時任務要執行
//發生以上幾種情況任一種則直接返回
break;
}
//此處的邏輯就是: 當前時間 - 循環開始時間 >= 定時select的時間timeoutMillis,說明已經執行過一次阻塞select(), 有效的select
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
//進入這個分支,表示超時,屬于正常的場景
//說明發生過一次阻塞式輪詢, 并且超時
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
//進入這個分支,表示沒有超時,同時 selectedKeys==0
//屬于異常場景
//表示啟用了select bug修復機制,
//即配置的io.netty.selectorAutoRebuildThreshold
//參數大于3,且上面select方法提前返回次數已經大于
//配置的閾值,則會觸發selector重建
//進行selector重建
//重建完之后,嘗試調用非阻塞版本select一次,并直接返回
selector = this.selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;
浙公網安備 33010602011771號