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

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

核心組件:
- Channel:雙向通信通道(如SocketChannel),數(shù)據(jù)可流入流出
- Buffer:數(shù)據(jù)緩沖區(qū),是雙向的,可讀可寫
- Selector:一個(gè)Selector對(duì)應(yīng)一個(gè)線程,一個(gè)Selector上可注冊(cè)多個(gè)Channel,并輪詢多個(gè)Channel的就緒事件
優(yōu)缺點(diǎn):
- 可以減少線程數(shù)量,降低線程切換的開銷,適用于需要處理大量并發(fā)連接的場(chǎng)景
- 缺點(diǎn):實(shí)現(xiàn)復(fù)雜度高
使用于高并發(fā)、長(zhǎng)連接的場(chǎng)景,如即時(shí)通訊場(chǎng)景
AIO(Asynchronous I/O)
異步非阻塞模型,基于事件回調(diào)或Future機(jī)制
- 調(diào)用方發(fā)起IO請(qǐng)求后,無需等待操作完成,可繼續(xù)執(zhí)行其他任務(wù)。操作系統(tǒng)在IO操作完成后,通過回調(diào)或事件通知的方式告知調(diào)用方
- Java中
AsynchronousSocketChannel是AIO的代表類,通過回調(diào)函數(shù)處理讀寫操作完成后的結(jié)果
優(yōu)缺點(diǎn):
- IO密集型的應(yīng)用,AIO提供更高的并發(fā)和低延遲,因?yàn)檎{(diào)用方在等待IO時(shí)不會(huì)被阻塞
- 缺點(diǎn):實(shí)現(xiàn)復(fù)雜
適用于高吞吐、低延遲的場(chǎng)景,如日志批量寫入
4.什么是Netty
說起Java的IO模型,繞不開的就是Netty框架了,那什么是Netty,為什么Netty的性能這么高呢?
- Netty是由JBOSS提供的一個(gè)Java開源框架。提供異步的、事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)用程序框架和工具,用以快速開發(fā)高性能、高可靠性的網(wǎng)絡(luò)服務(wù)器
- Netty的原理就是NIO,是基于NIO的完美封裝
很多中間件的底層通信框架用的都是它,比如:RocketMQ、Dubbo、Elasticsearch
4.1 Netty的核心要點(diǎn)
核心特點(diǎn):
- 高并發(fā):通過多路復(fù)用Selector實(shí)現(xiàn)單線程管理大量連接,減少線程開銷
- 傳輸快:零拷貝技術(shù),減少內(nèi)存拷貝次數(shù)
- 封裝性:簡(jiǎn)化NIO的復(fù)雜API,提供鏈?zhǔn)教幚恚–hannelPipeline)和可擴(kuò)展的編解碼能力(如Protobuf支持)
高性能的核心原因:
- 主從Reactor線程模型,無鎖化設(shè)計(jì),減少線程競(jìng)爭(zhēng)
- 零拷貝技術(shù),堆外內(nèi)存直接操作
- 高效內(nèi)存管理,對(duì)象池技術(shù),預(yù)分配內(nèi)存塊并復(fù)用,對(duì)象復(fù)用機(jī)制
- 基于Selector的I/O多路復(fù)用,異步事件驅(qū)動(dòng)機(jī)制
- Selector空輪詢問題修復(fù)
4.2 零拷貝技術(shù)
Netty的零拷貝體現(xiàn)在操作數(shù)據(jù)時(shí), 不需要將數(shù)據(jù) buffer從 一個(gè)內(nèi)存區(qū)域拷貝到另一個(gè)內(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)會(huì)把磁盤上的數(shù)據(jù)以 Page 為單位緩存在操作系統(tǒng)的內(nèi)存里
- 內(nèi)核緩沖區(qū)到 Socket 緩沖區(qū)之間并沒有做數(shù)據(jù)的拷貝,只是一個(gè)地址的映射,底層的網(wǎng)卡驅(qū)動(dòng)程序要讀取數(shù)據(jù)并發(fā)送到網(wǎng)絡(luò)上的時(shí)候,看似讀取的是 Socket 的緩沖區(qū)中的數(shù)據(jù),其實(shí)直接讀的是內(nèi)核緩沖區(qū)中的數(shù)據(jù)。
- 零拷貝中所謂的“零”指的是內(nèi)存中數(shù)據(jù)拷貝的次數(shù)為 0
4.2.2 Netty零拷貝技術(shù)
- 使用了堆外內(nèi)存進(jìn)行Socket讀寫,避免JVM堆內(nèi)存到堆外內(nèi)存的數(shù)據(jù)拷貝
- 提供了CompositeByteBuf合并對(duì)象,可以組合多個(gè)Buffer對(duì)象合并成一個(gè)邏輯上的對(duì)象,用戶可以像操作一個(gè)Buffer那樣對(duì)組合Buffer進(jìn)行操作,避免傳統(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ù),多個(gè)連接共用一個(gè)多路復(fù)用器,程序只需要阻塞等待多路復(fù)用器即可
- 基于線程池技術(shù)復(fù)用線程資源,程序?qū)⑦B接上的任務(wù)分配給線程池中線程處理,不用為每個(gè)連接單獨(dú)創(chuàng)建線程
- Reactor是圖中的ServiceHandler,在一個(gè)單獨(dú)線程中運(yùn)行,負(fù)責(zé)監(jiān)聽和分發(fā)事件
Reactor可以分為單Reactor單線程模式、單Reactor多線程模型,主從Reactor多線程模型
4.3.1 單Reactor單線程模式

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

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

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

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

