學習Netty之前,首先先掌握這些基礎知識:阻塞(Block)與非阻塞(Non-Block),同步(Synchronous)與異步(Asynchronous),Java BIO與NIO對比。
基礎概念
阻塞(Block)與非阻塞(Non-Block)
?阻塞模式?
- 調用I/O操作時,若數據未就緒,調用線程會被操作系統掛起(進入睡眠狀態)
- 線程會一直等待,直到內核緩沖區數據就緒后被喚醒
- 典型例子:
read()
/write()
默認的套接字操作
?非阻塞模式?
- 調用I/O操作時,無論數據是否就緒都會立即返回
- 需要通過輪詢(如
select
/epoll
)或回調機制主動檢查狀態 - 典型例子:設置
O_NONBLOCK
標志的套接字
同步(Synchronous)與異步(Asynchronous)
同步
- 應用程序?主動參與? I/O 操作的執行過程,必須等待操作完成才能繼續執行后續代碼
- 調用 I/O 函數(如?
read()
/write()/傳統阻塞式 Socket
(默認模式
))時,線程會阻塞直到操作完成。
注意?:同步 ≠ 阻塞(例如非阻塞輪詢也是同步行為)
異步
- 應用程序?發起 I/O 請求后立即返回?,操作系統負責完成實際操作
- 等待通知:通過回調(Callback)、事件通知(如?
epoll
)、信號(Signal)或 Future/Promise 機制獲取結果
?關鍵對比總結?
特性 | 同步 | 異步 |
---|---|---|
?控制流? | 應用程序主動等待結果 | 操作系統完成后通知應用程序 |
?線程阻塞? | 可能阻塞(取決于實現) | 絕不阻塞調用線程 |
?實現機制? | 阻塞調用/輪詢 | 回調/事件循環/信號 |
?性能影響? | 可能造成線程閑置 | 更高吞吐量 |
Java BIO與NIO
?BIO(同步阻塞IO)
??核心類庫??
- ServerSocket:監聽TCP連接,阻塞式接受請求?
- Socket:客戶端通信通道,讀寫操作均阻塞?
- BufferedReader/BufferedWriter:帶緩沖的字符流,減少IO次數?
?基礎特性?
- 基于
java.io
包的流模型,采用?同步阻塞?方式操作數據流(如InputStream
/OutputStream
)? - 線程在讀寫操作時會?完全阻塞?,直到數據就緒,導致資源閑置?
- 基于
?典型問題?
- ?單線程阻塞?:處理大文件或網絡延遲時,線程被長時間占用?
- ?擴展性差?:高并發需為每個連接創建線程,易引發線程爆炸?
?適用場景?
- 低并發、簡單IO操作(如本地文件讀寫)?
?NIO(同步非阻塞IO)?
?核心組件?
- ?Channel?:雙向通信管道(如
SocketChannel
),支持非阻塞模式? - ?Buffer?:數據容器(如
ByteBuffer
),所有讀寫操作通過緩沖區完成? - ?Selector?:多路復用器,單線程可監聽多個通道事件(如讀/寫就緒)?
- ?Channel?:雙向通信管道(如
?性能優勢?
- ?非阻塞?:線程可輪詢多個通道,避免無效等待?
- ?高并發?:單線程管理多個連接,減少線程切換開銷?
?適用場景?
- 高負載網絡應用(如Web服務器、即時通訊)
BIO的通信模型
ServerSocket和Socket是如何工作的(通信流程)
BIO單線程通信模型
- ServerSocket在服務器端綁定端口持續監聽,等待客戶端通信。
- 當客戶端Socket通過IP+端口發起連接請求。
- ServerSocket觸發accept()后生成專用的socket。
- 服務端和客戶端通過socket的I/o流進行雙向通信。
Acceptor線程模型(多線程BIO)
??核心組件分工?
?Acceptor線程?
獨立運行于while(true)
循環中,通過ServerSocket.accept()
阻塞監聽連接請求。一旦接收到新連接,生成對應的Socket
對象并將其封裝為任務提交至?Worker線程池??12。?Worker線程池?
由固定數量的線程組成,負責從任務隊列中獲取Socket
任務,執行同步阻塞式的I/O讀寫(如InputStream.read()
/OutputStream.write()
)。線程池通過復用線程避免頻繁創建銷毀的開銷?34。
底層執行流程?
?初始化階段?
- 啟動
ServerSocket
綁定端口,創建獨立的Acceptor線程和Worker線程池?。 - Acceptor線程進入監聽狀態,Worker線程池處于待命狀態?。
- 啟動
?連接處理階段?
- ?步驟1?:客戶端發起連接請求,Acceptor線程的
accept()
返回新Socket
?。 - ?步驟2?:Acceptor將
Socket
包裝為Runnable
任務(如SocketHandler
),提交至線程池的任務隊列?。 - ?步驟3?:Worker線程從隊列中獲取任務,處理該連接的I/O操作(讀寫數據),期間線程被阻塞直至操作完成?。
- ?步驟1?:客戶端發起連接請求,Acceptor線程的
?資源釋放階段?
- I/O操作完成后,Worker線程關閉
Socket
并返回線程池,等待下一個任務?。 - 若客戶端斷開連接,線程會主動釋放相關資源?
- I/O操作完成后,Worker線程關閉
對比?
?特性? | 單線程模型 | Acceptor線程模型 |
---|---|---|
?線程數量? | 1個線程處理所有連接 | 1個Acceptor線程 + N個Worker工作線程? |
?阻塞點? | accept() 和讀寫均阻塞 | 僅工作線程的讀寫阻塞? |
?適用場景? | 單客戶端測試 | 低并發生產環境? |
?連接監聽 | 主線程直接阻塞在accept() | 獨立Acceptor線程專責監聽? |
?I/O處理 | 同一線程串行處理所有連接I/O | Worker線程池并行處理? |
?資源消耗 | 線程少但完全阻塞 | 線程池復用,可控但仍有阻塞? |
?連接建立與處理? | 串行執行,新連接需等待舊連接處理完畢 | 并行處理,連接建立與業務解耦 |
?崩潰影響范圍? | 線程崩潰導致整個服務不可用 | 各階段隔離,局部崩潰不影響其他功能 |
Acceptor線程模型其實可以分為兩個階段
?第一階段(Acceptor線程)?
- ?專注連接建立?:通過
while(true)
循環持續調用ServerSocket.accept()
來監聽連接請求。- ?高優先級?:確保連接入口的高吞吐量,避免業務邏輯阻塞連接接收。
- ?崩潰隔離?:如果Acceptor線程崩潰(如端口被占用),?不會影響已建立的Socket連接?(Worker線程仍在運行)。
?第二階段(Worker線程池)?
- ?專注業務處理?:處理Socket的I/O操作(如HTTP請求解析、數據庫查詢等)。
- ?崩潰隔離?:若某個Worker線程崩潰(如業務邏輯異常),?不會影響Acceptor線程和其他Worker線程?,線程池會回收異常線程并新建替補線程(取決于線程池配置)。
Acceptor線程模型升級版
若Worker線程池滿載或處理緩慢,Acceptor線程仍持續提交新連接任務,可能導致線程池隊列溢出或拒絕連接?。可以采用Acceptor+線程池 或者NIO(Reactor模型)
Acceptor+線程池
- ?Acceptor線程?:通過
while(true)
循環監聽連接,接收后立即將Socket交給線程池處理?。 - ?Worker線程池?:使用
ThreadPoolExecutor
處理業務邏輯,需配置核心參數(核心線程數、隊列類型、拒絕策略等)?
PS:ServerSocket.accept() 為什么會出現阻塞?
TCP三次握手等待?
當客戶端發起連接時,需完成SYN→SYN-ACK→ACK的握手過程。若握手未完成,內核無法將連接放入"已完成連接隊列",導致accept()
持續等待?。?內核隊列狀態依賴?
accept()
實際是從內核維護的ESTABLISHED狀態隊列中提取連接。若隊列為空(無已完成握手的連接),調用線程會被掛起?35。?同步設計特性?
作為阻塞式I/O的標準行為,該設計避免了線程輪詢的CPU浪費,但會犧牲響應性?
NIO的通信模型
------------------------------------
NIO的工作過程
當Channel注冊到Selector上并聲明監聽類型后,Selector會持續監控所有已注冊的Channel。 selector會監控Channel的I/O就緒狀態,如果為就緒了將會放到就緒集合。之后獲取就緒集合對其進行后續的事件處理。
IO多路復用
舉例說明:柜員(線程)?快速巡視?各個窗口(Channel)
只有亮起提示燈的窗口(就緒事件)才需要處理
處理完一個窗口立即檢查下一個,?不等待客戶緩慢填單?(非阻塞操作)
總結:多路復用其實就是“一對多”,只要就緒了就對其進行處理。傳統的IO模型其實是“一對一”
select底層實現
select實現多路復用的核心分為三個階段:準備監控、輪詢掃描、事件處理,整個過程圍繞fd_set位圖機制展開。?
?準備階段?
- 用戶通過
FD_SET
宏將待監控的文件描述符(fd)加入fd_set位圖 - 提前標記需要關注的fd(如網絡socket)
- 用戶通過
?輪詢階段?
- 內核通過系統調用全量掃描位圖中的所有fd
- 通過位圖狀態位判斷哪些fd已就緒(如可讀/可寫)
- 相當于"服務員檢查所有餐桌"的阻塞過程
?處理階段?
- 用戶遍歷內核返回的就緒事件集合
- 對每個就緒fd執行對應IO操作
關于?select()
?的數據拷貝時機,其核心流程中的拷貝操作發生在兩個關鍵階段:
?調用?
select()
?時?
用戶態程序會將監控的?fd_set
?位圖(包含所有待監控的 fd)?完整拷貝到內核態?,這是全量掃描的前提。此時內核需要獲取完整的 fd 集合信息才能開始遍歷檢查。?返回結果時?
內核完成全量掃描后,會將?就緒的 fd 標記?在新的?fd_set
?位圖中,并將該結果位圖?拷貝回用戶態?。用戶程序需通過?FD_ISSET
?遍歷位圖解析就緒事件
select()底層原理總結:
select() 其實主要通過fd_set位圖進行對fd(文件描述符)進行監控,系統觸發全量fd掃描,根據fd的狀態監測就緒事件,最后對就緒事件進行處理。
epoll底層實現
epoll的工作流程可分為三個核心階段,結合紅黑樹和就緒隊列實現高效事件管理:
?初始化階段?
- 創建
epoll
實例(epoll_create
),內核會初始化紅黑樹和就緒鏈表結構 - 紅黑樹用于存儲所有待監控的fd(文件描述符),確保快速插入/刪除
- 創建
?事件注冊階段?
- 通過
epoll_ctl
將fd添加到紅黑樹,并設置回調函數(如讀/寫事件) - 網卡驅動通過中斷觸發回調,將活躍事件對應的節點移至就緒隊列
- 通過
?事件輪詢階段?
epoll_wait
系統調用直接獲取就緒隊列中的事件,無需全量掃描- 用戶空間僅處理實際活躍的連接,時間復雜度O(1)
?關鍵優化點?
- 零拷貝?:用戶態與內核態通過共享內存傳遞就緒事件(
典型實現如sendfile
系統調用,直接將文件數據從磁盤經內核緩沖區傳輸到網卡緩沖區 ) - 邊緣觸發(ET)?:減少重復事件通知,需配合非阻塞I/O使用
- LT/ET模式?:LT(水平觸發)會重復通知未處理事件,ET(邊緣觸發)僅通知一次
- 零拷貝?:用戶態與內核態通過共享內存傳遞就緒事件(
總結:
epoll通過?紅黑樹?和?就緒隊列?兩級數據結構實現高效事件管理:
- ?紅黑樹?作為全局存儲,維護所有待監控的文件描述符(fd)
- ?事件驅動機制?通過回調函數(如網卡中斷觸發)將活躍事件對應的fd節點移至?就緒隊列?;
- 最終
epoll_wait
僅需掃描就緒隊列中的活躍事件,避免全量fd集合的遍歷掃
epoll與select/poll的關鍵架構差異。具體表現為:
?epoll的就緒隊列機制?
- 內核通過紅黑樹管理監控fd集合,同時?獨立維護雙向鏈表結構的就緒隊列?
- 網卡中斷觸發回調時,內核直接將活躍事件對應的epitem節點插入隊列,無需遍歷檢查
epoll_wait
僅需讀取該隊列內容,時間復雜度恒為O(1)
?select/poll的臨時檢測機制?
- 每次調用時需?全量掃描監控的fd集合?,通過輪詢檢查每個fd狀態
- 返回的"就緒列表"是臨時生成的位圖(poll是revents數組),非持續維護結構
- 即使僅1個fd就緒,仍需完成O(n)次掃描
單線程的 Reactor 模型
工作流程
- ?事件監聽?:Reactor線程通過select/epoll輪詢監聽所有連接事件
- ?事件分發?:
- 連接事件 → 交給Acceptor處理(創建新連接并注冊Handler)
- 讀寫事件 → 分發給對應Handler處理
- ?業務處理?:Handler完成數據讀取→業務計算→響應發送的全流程
典型應用場景
- Redis的多路復用模型采用此模式
- 適合低并發、輕量級業務場景
總結:reactor主要是負責監聽和事件分發(Dispatcher分發事件),監聽通過多路復用器(底層使用select和epoll),而事件分發如果是 連接事件交給Acceptor處理(創建新連接并注冊Handler),讀寫事件的話直接交給handler處理。
事件分類?
?ACCEPT事件?:
- 接受客戶端連接
- 創建對應的SocketChannel
- 注冊READ事件到Selector
?READ事件?:
- 從Channel讀取數據到Buffer
- 解碼并處理業務邏輯
- 如有響應,注冊WRITE事件
?WRITE事件?:
- 將響應數據寫入Buffer
- 從Buffer寫入Channel
- 完成后取消WRITE事件注冊
PS:
事件處理流程?:
- 連接事件 → Reactor → Acceptor
? 創建SocketChannel
? 注冊READ事件到Selector
? 綁定對應Handler- 讀寫事件 → Reactor → Handler
? 讀:Channel→Buffer→業務處理
? 寫:業務數據→Buffer→Channel
思考:
為什么連接事件交給Acceptor處理?
Acceptor專職處理連接事件(ACCEPT),因其涉及底層系統調用(如bind/listen)和資源初始化,需與業務邏輯隔離;
為什么讀寫事件直接交給Handler處理?
讀寫事件直接由Handler處理,避免冗余中轉,實現數據到業務的最短路徑。這種分工既保障了連接穩定性,又提升了數據處理效率
selector中的SelectionKey?是什么東西,它作用是什么?
SelectionKey就是NIO中事件的通知單——它標記了哪個Channel觸發了什么事件(比如可讀/可寫),并攜帶對應的上下文數據,相當于Selector和Channel之間的"事件快遞員"
Handler的創建與綁定機制
Handler的創建和綁定主要通過以下方式確保:
?創建時機?:
- 在Acceptor接受新連接時創建(最常見)
- 預創建線程池中的Handler實例(多線程模型)
- 延遲初始化(按需創建)
?綁定方式?:
- 通過SelectionKey(attachment)關聯
對應的SelectionKey無有效attachment怎么處理
若SelectionKey.attachment()返回null或無效對象,多數實現會直接關閉該Channel并取消Key注冊,避免資源泄漏;
讀事件特殊性的處理?,即使無Handler,底層仍會觸發讀就緒通知,但數據會被丟棄(通過執行channel.read(ByteBuffer)
清空緩沖區)
最佳實踐建議?
- 在Acceptor階段必須顯式綁定Handler到新Channel
- 通過
key.isValid()
+attachment()!=null
雙重校驗防御無效事件 - 對異常Channel實現fallback處理策略(如重連機制)
事件觸發 → 檢查Key.attachment() →?存在Handler → 調用對應方法
不存在 → 關閉Channel或創建應急Handler
典型問題處理?
?事件堆積?
需及時處理就緒事件并調用selectedKeys().clear()
,否則會導致重復觸發。?內存泄漏?
長時間未關閉的Channel會導致SelectionKey堆積,需通過key.cancel()
主動釋放。?并發沖突?
多線程修改interestOps()
需同步(如使用selector.wakeup()
)
多線程的 Reactor 模型
?核心組件與職責?
?Reactor(反應器)?
- ?職責?:統一監聽所有I/O事件(連接、讀寫),通過I/O多路復用(如
epoll
)實現非阻塞監聽。 - ?線程模型?:由多個線程共同運行事件循環(EventLoop),每個線程獨立處理一組
Channel
。
- ?職責?:統一監聽所有I/O事件(連接、讀寫),通過I/O多路復用(如
?Handler(處理器)?
- ?職責?:執行數據讀寫和業務邏輯,通常與Reactor線程綁定,避免跨線程競爭。
- ?優化點?:若業務耗時較長,可提交至獨立Worker線程池處理。
?Worker線程池(可選)?
- ?職責?:異步處理耗時業務邏輯(如數據庫操作),與Reactor線程解耦
執行過程
- Reactor 通過監聽客戶端請求事件。
- 如果是連接事件,Acceptor 通過 accept 接受連接,并注冊到 Reactor 中,之后創建一個 Handler 處理后續事件。
- 如果是讀寫事件,Reactor 調用對應的 Handler 處理。
- Handler 只負責讀取數據,將業務處理交給 Worker 線程池。
- Worker 線程池 完成業務處理,將結果返回給 Handler,由 Handler 發送給客戶端。
當客戶端發起連接時,EventLoop會通過Selector檢測到ACCEPT事件,立即調用Acceptor接收新連接,并將生成Channel的注冊到自己的Selector上監聽讀事件。當Channel有數據到達觸發OP_READ時,EventLoop會調用Handler讀取數據,然后將需要復雜計算的業務邏輯提交給Worker線程池處理。Worker線程完成計算后,通過任務隊列將結果回調給EventLoop線程,最終由EventLoop負責將結果寫回客戶端。整個過程就像高效的流水線,EventLoop專注I/O調度,耗時操作交給線程池,既保證了響應速度又提升了吞吐量。
非主從模型中?:只有一個EventLoop線程負責所有事件的監聽和分發
EventLoop與線程的對應關系?
模型類型 | EventLoop數量 | 線程數量 | 任務處理方式 |
---|---|---|---|
?單線程Reactor? | 1個 | 1個 | 所有任務串行執行 |
?多線程Reactor? | 1個 | N+1個 | EventLoop線程 + Worker線程池 |
Worker線程池的核心作用?
?職責分離原則?
- EventLoop線程僅處理IO事件(如epoll事件監聽)
- Worker線程池專職處理?耗時業務邏輯?(如數據庫查詢、復雜計算)26
?性能優化關鍵?
- 避免阻塞EventLoop線程,保證IO事件響應速度
- 通過線程池復用線程資源,降低線程創建銷毀開銷
經典問題解析
Q1:EventLoop如何保證線程安全?
通過任務隊列的互斥鎖(如pthread_mutex_t)實現生產-消費模型,EventLoop線程與Worker線程無共享狀態Q2:EventLoop卡頓怎么辦?
監控任務隊列積壓,動態擴容Worker線程池或拆分耗時任務Q3:Netty中如何實現?
通過NioEventLoopGroup管理I/O線程組,單個NioEventLoop綁定一個Selector處理多ChannelQ4:如何配置Worker線程池的最佳參數
核心參數配置策略?得考慮是CPU密集型任務?還是I/O密集型任務不同的任務類型需要不同的配置,同時還得考慮拒絕策略選擇。Q5:Selector是如何檢測ACCEPT事件的?
epoll機制或者select機制
主從多線程的 Reactor 模型
主從模型中主Reactor和子Reactor如何協作
主從Reactor模型的協作機制通過分層分工實現高效事件處理,具體流程如下:
1. ?主Reactor職責?
- ?連接監聽?:主Reactor(Main-Reactor)由獨立線程運行,通過
Acceptor
監聽服務端端口,專門處理新連接請求。 - ?連接分配?:當新連接到達時,主Reactor調用
accept()
創建SocketChannel
,并通過輪詢或負載均衡策略將其動態注冊到某個子Reactor的Selector
上。 - ?非阻塞設計?:主Reactor僅處理連接事件,不參與I/O操作,避免阻塞后續連接請求。
2. ?子Reactor職責?
- ?事件監聽?:每個子Reactor(Sub-Reactor)運行在獨立線程中,通過多路復用器(如
epoll
)監聽已注冊Channel
的讀寫事件。 - ?任務分發?:當檢測到I/O事件時,子Reactor將任務(如讀/寫操作)封裝為回調函數,壓入任務隊列異步處理。
- ?優先級處理?:優先處理簡單I/O事件,耗時任務(如業務邏輯)交由線程池(Worker)執行,避免阻塞事件循環。
3. ?協作流程示例?
- ?連接階段?:客戶端請求到達 → 主Reactor的
Acceptor
接收連接 → 分配至子Reactor。 - ?I/O階段?:子Reactor監聽連接事件 → 觸發
Handler
讀取數據 → 線程池處理業務邏輯 →?Handler
回寫結果。 - ?資源隔離?:主/子Reactor線程互不干擾,子Reactor間通過任務隊列實現負載均衡
談談你對Reactor模型的理解
Reactor模型本質上是一種高效的事件驅動架構,它通過I/O多路復用技術(如select/epoll)實現單線程監聽大量網絡連接,將就緒事件分發給對應的處理器執行。這種模型的核心價值在于解決了傳統阻塞IO中"一連接一線程"的資源浪費問題,通過主從Reactor的分層設計——主線程專注接收新連接,子線程組處理已建立連接的I/O事件,再結合業務線程池異步執行耗時操作,形成連接處理、I/O操作與業務邏輯的三級流水線。在實際工程中(如Netty框架),這種設計既能保證高并發場景下的吞吐量,又通過Channel與EventLoop的綁定機制維持線程安全,使得系統在應對百萬級連接時仍能保持優雅的擴展性。