寫在前面:2020年面試必備的Java后端進(jìn)階面試題總結(jié)了一份復(fù)習(xí)指南在Github上爬泥,內(nèi)容詳細(xì),圖文并茂,有需要學(xué)習(xí)的朋友可以Star一下队他!
GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master
1、I/O阻塞
書上說BIO峻村、NIO等都屬于I/O模型,但是I/O模型這個(gè)范圍有點(diǎn)含糊锡凝,我為此走了不少彎路粘昨。我們?nèi)粘i_發(fā)過程中涉及到NIO模型應(yīng)用,如Tomcat窜锯、Netty中等線程模型张肾,可以直接將其視為 網(wǎng)絡(luò)I/O模型 。本文還是在基礎(chǔ)篇章中介紹幾種I/O模型方式锚扎,后面就默認(rèn)只講解網(wǎng)絡(luò)I/O模型了吞瞪。
1.1、I/O分類
BIO驾孔、NIO芍秆、AIO等都屬于I/O模型,所以它們優(yōu)化的都是系統(tǒng)I/O的性能翠勉,因此首先妖啥,我們要清楚常見的I/O有哪些分類:
1.2、I/O過程和性能
I/O(Input/Output)即數(shù)據(jù)的輸入/輸出对碌,為什么大家很關(guān)心I/O的性能呢荆虱?因?yàn)镮/O存在的范圍很廣,在高并發(fā)的場景下朽们,這部分性能會(huì)被無限放大怀读。而且與業(yè)務(wù)無關(guān),是可以有統(tǒng)一解決方案的骑脱。
所有的系統(tǒng)I/O都分為兩個(gè)階段:等待就緒和數(shù)據(jù)操作菜枷。舉例來說,讀函數(shù)惜姐,分為等待系統(tǒng)可讀和真正的讀犁跪;同理,寫函數(shù)分為等待網(wǎng)卡可以寫和真正的寫:
- 等待就緒 :等待數(shù)據(jù)就緒歹袁,一般是將數(shù)據(jù)加載到 內(nèi)核緩存區(qū) 坷衍。無論是從磁盤、網(wǎng)絡(luò)讀取數(shù)據(jù)条舔,程序能處理的都是進(jìn)入內(nèi)核態(tài)之后的數(shù)據(jù)枫耳,在這之前,cpu會(huì)阻塞住孟抗,等待數(shù)據(jù)進(jìn)入內(nèi)核態(tài)迁杨。
- 數(shù)據(jù)操作 :數(shù)據(jù)就緒后钻心,一般是將內(nèi)核緩存中的數(shù)據(jù)加載到 用戶緩存區(qū) 。
需要說明的是等待就緒的阻塞是不使用CPU的铅协,是在“空等”捷沸;而真正的讀寫操作的阻塞是使用CPU的,真正在”干活”狐史,而且這個(gè)過程非逞鞲快,屬于memory copy骏全,帶寬通常在1GB/s級別以上苍柏,可以理解為基本不耗時(shí)。這就出現(xiàn)一個(gè)奇怪的現(xiàn)象 -- 不使用CPU的“等待就緒”姜贡,卻比實(shí)際使用CPU的“數(shù)據(jù)操作”试吁,占用CPU時(shí)間更多 。
傳統(tǒng)阻塞I/O模型楼咳,即在讀寫數(shù)據(jù)過程中會(huì)發(fā)生阻塞現(xiàn)象熄捍。當(dāng)用戶線程發(fā)出I/O請求之后,內(nèi)核會(huì)去查看數(shù)據(jù)是否就緒爬橡,如果沒有就緒就會(huì)等待數(shù)據(jù)就緒治唤,而用戶線程就會(huì)處于阻塞狀態(tài),用戶線程交出CPU糙申。當(dāng)數(shù)據(jù)就緒之后宾添,內(nèi)核會(huì)將數(shù)據(jù)拷貝到用戶線程,并返回結(jié)果給用戶線程柜裸,用戶線程才會(huì)解除block狀態(tài)缕陕。
明確的是,讓當(dāng)前工作線程阻塞疙挺,等待數(shù)據(jù)就緒扛邑,是很浪費(fèi)線程資源的事情,上述三種I/O都有一定的優(yōu)化方案:
- 磁盤I/O :現(xiàn)代電腦中都有一個(gè)DMA(Direct Memory Access 直接內(nèi)存訪問) 的外設(shè)組件铐然,可以將I/O數(shù)據(jù)直接傳送到主存儲(chǔ)器中并且傳輸不需要CPU的參與蔬崩,以此將CPU解放出來去完成其他的事情。
- 網(wǎng)絡(luò)I/O :NIO搀暑、AIO等I/O模型沥阳,通過向事件選擇器注冊I/O事件,基于就緒的事情來驅(qū)動(dòng)執(zhí)行I/O操作自点,避免的等待過程桐罕。
- 內(nèi)存I/O :內(nèi)存部分沒涉及到太多阻塞,優(yōu)化點(diǎn)在于減少用戶態(tài)和內(nèi)核態(tài)之間的數(shù)據(jù)拷貝。nio中的零拷貝就有mmap和sendfile等實(shí)現(xiàn)方案功炮。
1.3溅潜、網(wǎng)絡(luò)I/O阻塞
這里仔細(xì)的講講網(wǎng)絡(luò)I/O模型中的阻塞,即socket的阻塞薪伏。在計(jì)算機(jī)通信領(lǐng)域滚澜,socket 被翻譯為“套接字”,它是計(jì)算機(jī)之間進(jìn)行通信的一種約定或一種方式嫁怀,是在tcp/ip協(xié)議上博秫,抽象出來的一層網(wǎng)絡(luò)通訊協(xié)議。
同上面I/O的過程一樣眶掌,網(wǎng)絡(luò)I/O也同樣分成兩個(gè)部分:
- 等待網(wǎng)絡(luò)數(shù)據(jù)到達(dá)網(wǎng)卡,讀取到內(nèi)核緩沖區(qū)巴碗。
- 從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù)到用戶態(tài)空間朴爬。
每個(gè) socket 被創(chuàng)建后,都會(huì)分配兩個(gè)緩沖區(qū)橡淆,輸入緩沖區(qū)和輸出緩沖區(qū):
- 輸入緩沖區(qū) :當(dāng)使用 read()/recv() 讀取數(shù)據(jù)時(shí)召噩,(1)首先會(huì)檢查緩沖區(qū),如果緩沖區(qū)中有數(shù)據(jù)逸爵,那么就讀取具滴,否則函數(shù)會(huì)被阻塞,直到網(wǎng)絡(luò)上有數(shù)據(jù)到來师倔。(2)如果要讀取的數(shù)據(jù)長度小于緩沖區(qū)中的數(shù)據(jù)長度构韵,那么就不能一次性將緩沖區(qū)中的所有數(shù)據(jù)讀出,剩余數(shù)據(jù)將不斷積壓趋艘,直到有 read()/recv() 函數(shù)再次讀取疲恢。(3)直到讀取到數(shù)據(jù)后 read()/recv() 函數(shù)才會(huì)返回,否則就一直被阻塞瓷胧。
- 輸出緩沖區(qū) :當(dāng)使用 write()/send() 發(fā)送數(shù)據(jù)時(shí)显拳,(1)首先會(huì)檢查緩沖區(qū),如果緩沖區(qū)的可用空間長度小于要發(fā)送的數(shù)據(jù)搓萧,那么 write()/send() 會(huì)被阻塞(暫停執(zhí)行)杂数,直到緩沖區(qū)中的數(shù)據(jù)被發(fā)送到目標(biāo)機(jī)器,騰出足夠的空間瘸洛,才喚醒 write()/send() 函數(shù)繼續(xù)寫入數(shù)據(jù)揍移。(2) 如果TCP協(xié)議正在向網(wǎng)絡(luò)發(fā)送數(shù)據(jù),那么輸出緩沖區(qū)會(huì)被鎖定货矮,不允許寫入羊精,write()/send() 也會(huì)被阻塞,直到數(shù)據(jù)發(fā)送完畢緩沖區(qū)解鎖,write()/send() 才會(huì)被喚醒喧锦。(3)如果要寫入的數(shù)據(jù)大于緩沖區(qū)的最大長度读规,那么將分批寫入。(4)直到所有數(shù)據(jù)被寫入緩沖區(qū) write()/send() 才能返回燃少。
由此可見在網(wǎng)絡(luò)I/O中束亏,會(huì)有很多的因素導(dǎo)致數(shù)據(jù)的讀取和寫入過程出現(xiàn)阻塞,創(chuàng)建socket連接也一樣阵具。socket.accept()碍遍、socket.read()、socket.write()這類函數(shù)都是同步阻塞的阳液,當(dāng)一個(gè)連接在處理I/O的時(shí)候怕敬,系統(tǒng)是阻塞的,該線程當(dāng)前的cpu時(shí)間片就浪費(fèi)了帘皿。
2东跪、阻塞優(yōu)化
2.1、BIO鹰溜、NIO虽填、AIO
BIO、NIO曹动、AIO對比
以socket.read()為例子:
- 傳統(tǒng)的BIO里面socket.read()斋日,如果TCP RecvBuffer里沒有數(shù)據(jù),函數(shù)會(huì)一直阻塞墓陈,直到收到數(shù)據(jù)恶守,返回讀到的數(shù)據(jù)。
- 對于NIO跛蛋,如果TCP RecvBuffer有數(shù)據(jù)熬的,就把數(shù)據(jù)從網(wǎng)卡讀到內(nèi)存,并且返回給用戶赊级;反之則直接返回0押框,永遠(yuǎn)不會(huì)阻塞。
- 最新的AIO(Async I/O)里面會(huì)更進(jìn)一步:不但等待就緒是非阻塞的理逊,就連數(shù)據(jù)從網(wǎng)卡到內(nèi)存的過程也是異步的橡伞。
換句話說,BIO里用戶最關(guān)心“我要讀”晋被,NIO里用戶最關(guān)心”我可以讀了”兑徘,在AIO模型里用戶更需要關(guān)注的是“讀完了”。
NIO
NIO的優(yōu)化體現(xiàn)在兩個(gè)方面:
- 網(wǎng)絡(luò)I/O模式 的優(yōu)化羡洛,通過非阻塞的模式挂脑,提高了CPU的使用性能。
- 內(nèi)存I/O 的優(yōu)化,零拷貝等方式崭闲,讓數(shù)據(jù)在內(nèi)核態(tài)和用戶態(tài)之前的傳輸消耗降低了肋联。
NIO一個(gè)重要的特點(diǎn)是: socket主要的讀、寫刁俭、注冊和接收函數(shù)橄仍,在等待就緒階段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高) 牍戚。
NIO的主要事件有幾個(gè):讀就緒侮繁、寫就緒、有新連接到來如孝。
我們首先需要注冊當(dāng)這幾個(gè)事件到來的時(shí)候所對應(yīng)的處理器宪哩。然后在合適的時(shí)機(jī)告訴事件選擇器:我對這個(gè)事件感興趣。對于寫操作第晰,就是寫不出去的時(shí)候?qū)懯录信d趣斋射;對于讀操作,就是完成連接和系統(tǒng)沒有辦法承載新讀入的數(shù)據(jù)的時(shí)但荤;對于accept,一般是服務(wù)器剛啟動(dòng)的時(shí)候涧至;而對于connect腹躁,一般是connect失敗需要重連或者直接異步調(diào)用connect的時(shí)候。
其次南蓬,用一個(gè)死循環(huán)選擇就緒的事件纺非,會(huì)執(zhí)行系統(tǒng)調(diào)用 (Linux 2.6之前是select、poll赘方,2.6之后是epoll烧颖,Windows是IOCP) ,還會(huì)阻塞的等待新事件的到來窄陡。新事件到來的時(shí)候炕淮,會(huì)在selector上注冊標(biāo)記位,標(biāo)示可讀跳夭、可寫或者有連接到來涂圆。
2.2、Reactor模式
Reactor模式稱之為響應(yīng)器模式币叹,通常用于 NIO 非阻塞IO的網(wǎng)絡(luò)通信框架中润歉。Reactor設(shè)計(jì)模式用于處理由一個(gè)或多個(gè)客戶端并發(fā)傳遞給應(yīng)用程序的的服務(wù)請求,可以理解成颈抚, Reactor模式是用來實(shí)現(xiàn)網(wǎng)絡(luò)NIO的方式 勃黍。
Reactor是一種事件驅(qū)動(dòng)機(jī)制宋舷,是處理并發(fā)I/O常見的一種模式谬运,用于同步I/O纱烘,其中心思想是將所有要處理的I/O事件注冊到一個(gè)中心I/O多路復(fù)用器上,同時(shí)主線程阻塞在多路復(fù)用器上欣舵,一旦有I/O事件到來或是準(zhǔn)備就緒,多路復(fù)用器將返回并將相應(yīng)I/O事件分發(fā)到對應(yīng)的處理器中。
Reactor模式主要分為下面三個(gè)部分:
- 事件接收器Acceptor :主要負(fù)責(zé)接收請求連接宴树,接收請求后,會(huì)將建立的連接注冊到分離器中晶疼。
- 事件分離器Reactor :依賴于循環(huán)監(jiān)聽多路復(fù)用器Selector酒贬,是阻塞的,一旦監(jiān)聽到事件翠霍,就會(huì)將事件分發(fā)到事件處理器锭吨。(例如:監(jiān)聽讀事件,等到內(nèi)核態(tài)數(shù)據(jù)就緒后寒匙,將事件分發(fā)到Handler零如,Handler將數(shù)據(jù)讀到用戶態(tài)再做處理)
- 事件處理器Handler :事件處理器主要完成相關(guān)的事件處理,比如讀寫I/O操作锄弱。
2.3考蕾、三種Reactor模式
單線程Reactor模式
一個(gè)線程:
- 單線程:建立連接(Acceptor)、監(jiān)聽accept会宪、read肖卧、write事件(Reactor)、處理事件(Handler)都只用一個(gè)單線程掸鹅。
多線程Reactor模式
一個(gè)線程 + 一個(gè)線程池:
- 單線程:建立連接(Acceptor)和 監(jiān)聽accept塞帐、read、write事件(Reactor)巍沙,復(fù)用一個(gè)線程葵姥。
- 工作線程池:處理事件(Handler),由一個(gè)工作線程池來執(zhí)行業(yè)務(wù)邏輯句携,包括數(shù)據(jù)就緒后榔幸,用戶態(tài)的數(shù)據(jù)讀寫。
主從Reactor模式
三個(gè)線程池:
- 主線程池:建立連接(Acceptor)矮嫉,并且將accept事件注冊到從線程池牡辽。
- 從線程池:監(jiān)聽accept、read敞临、write事件(Reactor)态辛,包括等待數(shù)據(jù)就緒時(shí),內(nèi)核態(tài)的數(shù)據(jù)I讀寫挺尿。
- 工作線程池:處理事件(Handler)奏黑,由一個(gè)工作線程池來執(zhí)行業(yè)務(wù)邏輯炊邦,包括數(shù)據(jù)就緒后,用戶態(tài)的數(shù)據(jù)讀寫熟史。
3馁害、Tomcat線程模型
3.1、Api網(wǎng)絡(luò)請求過程
我們先補(bǔ)一下基礎(chǔ)知識(shí)蹂匹,講解后端接口的響應(yīng)過程碘菜。一個(gè)http連接里,完整的網(wǎng)絡(luò)處理過程一般分為accept限寞、read忍啸、decode、process履植、encode计雌、send這幾步:
- accept :接收客戶端的連接請求,創(chuàng)建socket連接(tcp三次握手玫霎,創(chuàng)建連接)凿滤。
- read :從socket讀取數(shù)據(jù),包括等待讀就緒庶近,和實(shí)際讀數(shù)據(jù)翁脆。
- decode :解碼,因?yàn)榫W(wǎng)絡(luò)上的數(shù)據(jù)都是以byte的形式進(jìn)行傳輸?shù)谋侵郑氆@取真正的請求鹃祖,必定需要解碼。
- process :業(yè)務(wù)處理普舆,即服務(wù)端程序的業(yè)務(wù)邏輯實(shí)現(xiàn)。
- encode :編碼校读,同理沼侣,因?yàn)榫W(wǎng)絡(luò)上的數(shù)據(jù)都是以byte的形式進(jìn)行傳輸?shù)模簿褪莝ocket只接收byte歉秫,所以必定需要編碼蛾洛。
- send :往網(wǎng)絡(luò)socket寫回?cái)?shù)據(jù),包括實(shí)際寫數(shù)據(jù)雁芙,和等待寫就緒轧膘。
3.2、各個(gè)線程模型
在tomcat的各個(gè)版本中兔甘,所支持的線程模型也發(fā)生了一步步演變谎碍。一方面,直接將默認(rèn)線程模型洞焙,從BIO變成了NIO蟆淀。另一方面拯啦,在后續(xù)幾個(gè)版本中,加入了對AIO和APR線程模型的支持熔任,這里要注意褒链,僅僅是支持,而非默認(rèn)線程模型疑苔。
- BIO :阻塞式IO甫匹,tomcat7之前默認(rèn),采用傳統(tǒng)的java IO進(jìn)行操作惦费,該模式下每個(gè)請求都會(huì)創(chuàng)建一個(gè)線程兵迅,適用于并發(fā)量小的場景。
- NIO :同步非阻塞趁餐,比傳統(tǒng)BIO能更好的支持大并發(fā)喷兼,tomcat 8.0 后默認(rèn)采用該模式。
- AIO :異步非阻塞 (NIO2)后雷,tomcat8.0后支持季惯。多用于連接數(shù)目多且連接比較長(重操作)的架構(gòu),比如相冊服務(wù)器臀突,充分調(diào)用OS參與并發(fā)操作勉抓,編程比較復(fù)雜。
- APR :tomcat 以JNI形式調(diào)用http服務(wù)器的核心動(dòng)態(tài)鏈接庫來處理文件讀取或網(wǎng)絡(luò)傳輸操作候学,需要編譯安裝APR庫(也就是說IO操作的部分直接調(diào)用native代碼實(shí)現(xiàn))藕筋。
各個(gè)線程模型中,NIO是作為目前最實(shí)用的線程模型梳码,因此也是目前Tomcat默認(rèn)的線程模型隐圾,因此本文對此著重講解。
3.3掰茶、BIO和NIO
BIO模型
在BIO模型中暇藏,主要參與的角色有: Acceptor 和 Handler工作線程池 。對應(yīng)于前文中Api的請求過程濒蒋,它們的分工如下:
- Acceptor :Accepter線程專門負(fù)責(zé)建立網(wǎng)絡(luò)連接( accept )盐碱。新連接創(chuàng)建后,交給Handler工作線程池處理請求沪伙。
- Handlers :針對每個(gè)請求的連接瓮顽,Handler工作線程池都會(huì)分配一個(gè)線程,執(zhí)行后面的所有步驟( read围橡、decode暖混、process、encode翁授、send )儒恋。
前文的知識(shí)點(diǎn)有鋪墊善绎, read 和 send 是面向網(wǎng)絡(luò)I/O的,在等待讀寫就緒過程中诫尽,其實(shí)是CPU阻塞的禀酱。因此Handler工作線程池中的每個(gè)線程,都會(huì)因?yàn)镮/O阻塞而“空等待”牧嫉,造成浪費(fèi)剂跟。
NIO模型
tomcat的NIO模型,相比較于BIO模型酣藻,多了個(gè)Poller角色: Acceptor 曹洽、 Poller 和 Handler工作線程池 。這三個(gè)角色是不是很熟悉辽剧,如果將Poller換成Reactor送淆,是不是就是Reactor模型。沒錯(cuò)怕轿,tomcat的nio模型偷崩,的確就是基于 主從Reactor模型 ,只不過將Reactor換了個(gè)名字而已撞羽。
- Acceptor :Accepter線程專門負(fù)責(zé)建立網(wǎng)絡(luò)連接( accept )阐斜。新連接創(chuàng)建后,不是直接使用Worker線程處理請求诀紊,而是先將請求發(fā)送給Poller緩沖隊(duì)列谒出。
- Poller :在Poller中,維護(hù)了一個(gè)Selector對象邻奠,當(dāng)Poller從緩沖隊(duì)列中取出連接后笤喳,注冊到該Selector中,阻塞等待讀寫就緒( read等待就緒碌宴、send等待就緒 )杀狡。
- Handlers :遍歷Selector,找出其中就緒的IO操作唧喉,并交給Worker線程處理( read內(nèi)存讀、decode忍抽、process八孝、encode、send內(nèi)存寫 )鸠项。
對比
- BIO模型中干跛,一個(gè)線程對應(yīng)一個(gè)請求連接的完整過程,因此tomcat服務(wù)能處理的最大連接數(shù)祟绊,和最大線程數(shù)一致楼入。
- NIO模型中哥捕,在一個(gè)請求連接中,對應(yīng)的一個(gè)工作線程嘉熊,只處理I/O讀寫就緒后的非阻塞過程遥赚。因此tomcat服務(wù)能處理的最大連接數(shù),要遠(yuǎn)大于最大線程數(shù)量阐肤。
3.4凫佛、參數(shù)設(shè)置
針對于tomcat的nio模型,可以做一些參數(shù)設(shè)置孕惜。因?yàn)閟pringboot是內(nèi)嵌tomcat的愧薛,這些參數(shù)設(shè)置同樣可以在properties配置文件中定義:
- 最大線程數(shù)(server.tomcat.threads.max) :工作線程池的最大線程數(shù),默認(rèn)200衫画。注意不是越大越好毫炉,如果線程數(shù)過大,那么CPU會(huì)花費(fèi)大量的時(shí)間用于線程的切換削罩,整體效率會(huì)降低瞄勾。
- 最小線程數(shù)(server.tomcat.threads.min-spare) :工作線程池的最小線程數(shù),默認(rèn)10鲸郊。
- 最大等待數(shù)(server.tomcat.accept-count) :當(dāng)調(diào)用HTTP請求數(shù)達(dá)到tomcat的最大線程數(shù)時(shí)丰榴,還有新的HTTP請求到來,這時(shí)tomcat會(huì)將該請求放在等待隊(duì)列中秆撮,這個(gè)acceptCount就是指能夠接受的最大等待數(shù)四濒,默認(rèn)100。如果等待隊(duì)列也被放滿了职辨,這個(gè)時(shí)候再來新的請求就會(huì)被tomcat拒絕盗蟆。
- 最大連接數(shù)(server.tomcat.max-connections) :在同一時(shí)間,tomcat能夠接受的最大連接數(shù)舒裤,默認(rèn)8192喳资。
4、常見問題
1腾供、tomcat運(yùn)行后仆邓,出現(xiàn) nio-8080-exec- 前綴的線程作用是什么?
是工作線程池中的線程伴鳖。你們可以觀察某個(gè)springboot運(yùn)行項(xiàng)目的線程模型节值,由于基本都是基于nio模型的tomcat應(yīng)用,因此都包括這些線程:
- 1個(gè)名稱中包含Accepter的線程榜聂。
- 2個(gè)名稱中包含Poller的線程搞疗。
- 10個(gè)工作線程,名稱從 nio-8080-exec-1 到 nio-8080-exec-10须肆。如果并發(fā)交高匿乃,默認(rèn)最多有200個(gè)線程桩皿,名稱到 nio-8080-exec-200。
2幢炸、tomcat中nio模型中泄隔,存在poller單線程讀取多個(gè)請求線程的數(shù)據(jù),會(huì)不會(huì)出現(xiàn)線程安全問題阳懂?因?yàn)橥ㄟ^會(huì)使用ThreadLocal存儲(chǔ)請求用戶身份信息梅尤。
不會(huì)。因?yàn)閜oller只是處理等待讀就緒的環(huán)節(jié)岩调,一旦讀就緒事件觸發(fā)后巷燥,真正的讀取數(shù)據(jù)和處理業(yè)務(wù)邏輯,都是由工作線程池中的某個(gè)線程跟到底号枕,可以放心大膽使用ThreadLocal缰揪。
3、為什么我自己對比測試nio和bio葱淳,性能提升不大钝腺?
nio線程模型優(yōu)化的是線程利用率,為了在高并發(fā)場景下赞厕,基于有限的線程資源艳狐,處理更多的請求連接。
例如:tomcat使用默認(rèn)最大線程數(shù)200皿桑,但你的并發(fā)請求數(shù)量連200都不到毫目,就算是BIO模型,線程池中200個(gè)線程都沒利用完诲侮。這時(shí)候你用NIO還是BIO镀虐,區(qū)別不大,甚至BIO模型處理還更快一些沟绪。但如果你的并發(fā)請求數(shù)到了2000刮便、20000,BIO模型就會(huì)出現(xiàn)性能瓶頸了绽慈,超過200的請求都會(huì)阻塞住恨旱,而NIO模型就能大展身手。