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

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

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

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

- 基于IO多路復(fù)用技術(shù),多個連接共用一個多路復(fù)用器,程序只需要阻塞等待多路復(fù)用器即可
- 基于線程池技術(shù)復(fù)用線程資源,程序?qū)⑦B接上的任務(wù)分配給線程池中線程處理,不用為每個連接單獨創(chuàng)建線程
- Reactor是圖中的ServiceHandler,在一個單獨線程中運行,負責(zé)監(jiān)聽和分發(fā)事件
Reactor可以分為單Reactor單線程模式、單Reactor多線程模型,主從Reactor多線程模型
4.3.1 單Reactor單線程模式

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

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

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

- BossGroup:boss線程組,負責(zé)接收客戶端的連接請求,連接來了之后,將其注冊到Worker線程組的NioEventLoop中
- WorkerGroup:Worker線程組,每個線程都是一個NioEventLoop,負責(zé)和處理一個或多個Channel的I/O讀寫操作。處理邏輯通常是通過ChannelPipeline中的各個ChannelHandler來完成
- 業(yè)務(wù)線程組(可選):還可以引入一個業(yè)務(wù)線程組來處理業(yè)務(wù)邏輯,避免阻塞Worker線程
簡單理解:Boss線程是老板,Worker線程是員工,老板負責(zé)接收處理的事件請求,Worker負責(zé)工作,處理請求的I/O事件,并交給對應(yīng)的Handler處理
本質(zhì)是將線程連接和具體的業(yè)務(wù)處理分開
5.多路復(fù)用I/O的3種機制
5.1 select
這三種都是操作系統(tǒng)中的多路復(fù)用I/O機制
輪詢機制:select使用一個固定大小的位圖來表示文件描述符集,將文件描述符的狀態(tài)(如可讀、可寫)存儲在一個數(shù)組中,調(diào)用select時,每次需將完整的位圖從用戶空間拷貝到內(nèi)核空間,內(nèi)核遍歷所有描述符,檢查就緒狀態(tài)
局限:
- 文件描述符限制通常為1024,限制了并發(fā)處理數(shù)
- 性能低:搞并發(fā)場景,每次都要遍歷整個位圖,性能開銷大,時間負責(zé)度為O(N)
5.2 poll
poll使用了動態(tài)數(shù)組來替代位圖,使用pollfd結(jié)構(gòu)數(shù)組存儲文件描述符和事件,無數(shù)量限制
工作機制:每次調(diào)用時仍然需要遍歷所有描述符,即使只有少量描述符修改了,仍然要檢查整個數(shù)組,時間復(fù)雜度為O(N)
5.3 epoll
1)事件驅(qū)動模型:epoll使用紅黑樹來存儲和管理注冊的文件描述符,使用就緒事件鏈表來存儲觸發(fā)的事件。當(dāng)某個文件描述符上的事件就緒時,epoll會將該文件描述符添加到就緒鏈表中。
2)觸發(fā)模式:支持水平觸發(fā)(LT)和邊緣觸發(fā)(ET),ET模式下事件僅通知一次
- 水平觸發(fā)(Level Triggered),默認模式,只要文件描述符上有未處理的數(shù)據(jù),每次調(diào)用epoll_wait都會返回該文件描述符
- 邊緣觸發(fā)(Edge Triggered),僅在狀態(tài)發(fā)生變化時通知一次,減少重復(fù)事件的通知次數(shù)
3)工作流程:
epoll_create創(chuàng)建實例:分配相應(yīng)數(shù)據(jù)結(jié)構(gòu),并返回一個epoll文件描述符。內(nèi)核分配一棵紅黑樹管理文件描述符,以及一個就緒事件的鏈表epoll_ctl注冊、修改、刪除事件:epoll_ctl是用于管理文件描述符與事件關(guān)系的接口epoll_wait等待事件:epoll會檢查就緒事件鏈表,將鏈表中所有就緒的文件描述符返回給用戶空間。epoll_wait高效體現(xiàn)在它返回的是已經(jīng)發(fā)生事件的文件描述符,而不是遍歷所有注冊的文件描述符
優(yōu)點是時間復(fù)雜度O(1),僅處理活躍連接,性能和連接數(shù)無關(guān)
4)零拷貝機制:
- 通過內(nèi)存映射mmap減少了在內(nèi)核和用戶空間之間的數(shù)據(jù)復(fù)制,進一步提高了性能
總結(jié):epoll每次只傳遞發(fā)生的事件,不需要傳遞所有文件描述符,所以提高了效率
6. Netty如何解決JDK NIO空輪詢bug的?
Java NIO在Linux系統(tǒng)下默認是epoll機制,理論上無客戶端連接時Selector.select()方法是會阻塞的。
發(fā)生空輪詢bug表現(xiàn)時,即時select輪詢事件返回數(shù)量是0,Select.select()方法也不會被阻塞,NIO就會一直處于while死循環(huán)中,不斷向CPU申請資源導(dǎo)致CPU 100%
底層原因:
- Linux內(nèi)核在某些情況下會錯誤地將Selector的EPOLLUP(連接掛起)和EPOLLERR(錯誤)事件標(biāo)記為就緒狀態(tài),JDK中的NIO實現(xiàn)未正確處理這些事件,導(dǎo)致select()方法誤判事件存在而提前返回
6.1 Netty的解決方式
Netty并沒有解決這個bug,而是繞開了這個錯誤,具體如下:
1)統(tǒng)計空輪詢次數(shù):通過selectCnt計數(shù)器來統(tǒng)計連續(xù)空輪詢的次數(shù),每次執(zhí)行Selector.select()方法后,如果發(fā)現(xiàn)沒有IO事件,selectCnt就會遞增
2)設(shè)置閾值:定義了一個閾值,默認為512,當(dāng)空輪詢達到這個閾值時,Netty就會觸發(fā)重建Selector的操作
3)重建Selector:Netty新建一個Selector,并將所有注冊的Channel從舊的Selector轉(zhuǎn)移到新的Selector上,過程涉及取消舊Selector上的注冊,以及新Selector上重新注冊
4)關(guān)閉舊的Selector:重建Selector并將Channel重新注冊后,Netty關(guān)閉舊的Selector
總結(jié):通過SelectCnt統(tǒng)計沒有IO事件的次數(shù),來判斷當(dāng)前是否發(fā)生了空輪詢,如果發(fā)生了,就重建一個Selector來替換之前出問題的Selector
核心代碼如下:
long time = System.nanoTime();
//調(diào)用select方法,阻塞時間為上面算出的最近一個將要超時的定時任務(wù)時間
int selectedKeys = selector.select(timeoutMillis);
//計數(shù)器加1
++selectCnt;
if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
//進入這個分支,表示正常場景
//selectedKeys != 0: selectedKeys個數(shù)不為0, 有io事件發(fā)生
//oldWakenUp:表示進來時,已經(jīng)有其他地方對selector進行了喚醒操作
//wakenUp.get():也表示selector被喚醒
//hasTasks() || hasScheduledTasks():表示有任務(wù)或定時任務(wù)要執(zhí)行
//發(fā)生以上幾種情況任一種則直接返回
break;
}
//此處的邏輯就是: 當(dāng)前時間 - 循環(huán)開始時間 >= 定時select的時間timeoutMillis,說明已經(jīng)執(zhí)行過一次阻塞select(), 有效的select
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
//進入這個分支,表示超時,屬于正常的場景
//說明發(fā)生過一次阻塞式輪詢, 并且超時
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
//進入這個分支,表示沒有超時,同時 selectedKeys==0
//屬于異常場景
//表示啟用了select bug修復(fù)機制,
//即配置的io.netty.selectorAutoRebuildThreshold
//參數(shù)大于3,且上面select方法提前返回次數(shù)已經(jīng)大于
//配置的閾值,則會觸發(fā)selector重建
//進行selector重建
//重建完之后,嘗試調(diào)用非阻塞版本select一次,并直接返回
selector = this.selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;到此這篇關(guān)于Java的IO模型、Netty原理詳解的文章就介紹到這了,更多相關(guān)Java IO模型、Netty原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java集成開發(fā)SpringBoot生成接口文檔示例實現(xiàn)
這篇文章主要為大家介紹了java集成開發(fā)SpringBoot如何生成接口文檔的示例實現(xiàn)過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-10-10
java實現(xiàn)動態(tài)上傳多個文件并解決文件重名問題
這篇文章主要為大家詳細介紹了java實現(xiàn)動態(tài)上傳多個文件,并解決文件重名問題的方法,感興趣的小伙伴們可以參考一下2016-03-03
Triple協(xié)議支持Java異?;貍髟O(shè)計實現(xiàn)詳解
這篇文章主要為大家介紹了Triple協(xié)議支持Java異常回傳設(shè)計實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12
Java中@JSONField和@JsonProperty注解的用法及區(qū)別詳解
@JsonProperty和@JSONField注解都是為了解決obj轉(zhuǎn)json字符串的時候,將java bean的屬性名替換成目標(biāo)屬性名,下面這篇文章主要給大家介紹了關(guān)于Java中@JSONField和@JsonProperty注解的用法及區(qū)別的相關(guān)資料,需要的朋友可以參考下2024-06-06

