java NIO

NIO(Non-blocking I/O摩桶,在Java領(lǐng)域,也稱為New I/O)佩捞,是一種同步非阻塞的I/O模型,也是I/O多路復(fù)用的基礎(chǔ)字币,已經(jīng)被越來越多地應(yīng)用到大型應(yīng)用服務(wù)器,成為解決高并發(fā)與大量連接共缕、I/O處理問題的有效方式洗出。

那么NIO的本質(zhì)是什么樣的呢?它是怎樣與事件模型結(jié)合來解放線程图谷、提高系統(tǒng)吞吐的呢翩活?

本文會從傳統(tǒng)的阻塞I/O和線程池模型面臨的問題講起,然后對比幾種常見I/O模型便贵,一步步分析NIO怎么利用事件模型處理I/O菠镇,解決線程池瓶頸處理海量連接,包括利用面向事件的方式編寫服務(wù)端/客戶端程序承璃。最后延展到一些高級主題利耍,如Reactor與Proactor模型的對比、Selector的喚醒盔粹、Buffer的選擇等隘梨。

注:本文的代碼都是偽代碼,主要是為了示意舷嗡,不可用于生產(chǎn)環(huán)境轴猎。

傳統(tǒng)BIO模型分析

讓我們先回憶一下傳統(tǒng)的服務(wù)器端同步阻塞I/O處理(也就是BIO,Blocking I/O)的經(jīng)典編程模型:

{

ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//線程池

ServerSocket serverSocket = new ServerSocket();

serverSocket.bind(8088);

while(!Thread.currentThread.isInturrupted()){//主線程死循環(huán)等待新連接到來

Socket socket = serverSocket.accept();

executor.submit(new ConnectIOnHandler(socket));//為新的連接創(chuàng)建新的線程

}

class ConnectIOnHandler extends Thread{

? ? private Socket socket;

? ? public ConnectIOnHandler(Socket socket){

? ? ? this.socket = socket;

? ? }

? ? public void run(){

? ? ? while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循環(huán)處理讀寫事件

? ? ? ? ? String someThing = socket.read()....//讀取數(shù)據(jù)

? ? ? ? ? if(someThing!=null){

? ? ? ? ? ? ......//處理數(shù)據(jù)

? ? ? ? ? ? socket.write()....//寫數(shù)據(jù)

? ? ? ? ? }

? ? ? }

? ? }

}

這是一個經(jīng)典的每連接每線程的模型进萄,之所以使用多線程捻脖,主要原因在于socket.accept()、socket.read()中鼠、socket.write()三個主要函數(shù)都是同步阻塞的可婶,當一個連接在處理I/O的時候,系統(tǒng)是阻塞的兜蠕,如果是單線程的話必然就掛死在那里扰肌;但CPU是被釋放出來的,開啟多線程熊杨,就可以讓CPU去處理更多的事情曙旭。其實這也是所有使用多線程的本質(zhì):

利用多核。

當I/O阻塞系統(tǒng)晶府,但CPU空閑的時候桂躏,可以利用多線程使用CPU資源。

現(xiàn)在的多線程一般都使用線程池川陆,可以讓線程的創(chuàng)建和回收成本相對較低剂习。在活動連接數(shù)不是特別高(小于單機1000)的情況下,這種模型是比較不錯的,可以讓每一個連接專注于自己的I/O并且編程模型簡單鳞绕,也不用過多考慮系統(tǒng)的過載失仁、限流等問題。線程池本身就是一個天然的漏斗们何,可以緩沖一些系統(tǒng)處理不了的連接或請求萄焦。

不過,這個模型最本質(zhì)的問題在于冤竹,嚴重依賴于線程拂封。但線程是很"貴"的資源,主要表現(xiàn)在:

線程的創(chuàng)建和銷毀成本很高鹦蠕,在Linux這樣的操作系統(tǒng)中冒签,線程本質(zhì)上就是一個進程。創(chuàng)建和銷毀都是重量級的系統(tǒng)函數(shù)钟病。

線程本身占用較大內(nèi)存萧恕,像Java的線程棧,一般至少分配512K~1M的空間档悠,如果系統(tǒng)中的線程數(shù)過千廊鸥,恐怕整個JVM的內(nèi)存都會被吃掉一半。

線程的切換成本是很高的辖所。操作系統(tǒng)發(fā)生線程切換的時候惰说,需要保留線程的上下文,然后執(zhí)行系統(tǒng)調(diào)用缘回。如果線程數(shù)過高吆视,可能執(zhí)行線程切換的時間甚至?xí)笥诰€程執(zhí)行的時間,這時候帶來的表現(xiàn)往往是系統(tǒng)load偏高酥宴、CPU sy使用率特別高(超過20%以上)啦吧,導(dǎo)致系統(tǒng)幾乎陷入不可用的狀態(tài)。

容易造成鋸齒狀的系統(tǒng)負載拙寡。因為系統(tǒng)負載是用活動線程數(shù)或CPU核心數(shù)授滓,一旦線程數(shù)量高但外部網(wǎng)絡(luò)環(huán)境不是很穩(wěn)定,就很容易造成大量請求的結(jié)果同時返回肆糕,激活大量阻塞線程從而使系統(tǒng)負載壓力過大般堆。

所以,當面對十萬甚至百萬級連接的時候诚啃,傳統(tǒng)的BIO模型是無能為力的淮摔。隨著移動端應(yīng)用的興起和各種網(wǎng)絡(luò)游戲的盛行,百萬級長連接日趨普遍始赎,此時和橙,必然需要一種更高效的I/O處理模型仔燕。

NIO是怎么工作的

很多剛接觸NIO的人,第一眼看到的就是Java相對晦澀的API魔招,比如:Channel晰搀,Selector,Socket什么的仆百;然后就是一坨上百行的代碼來演示NIO的服務(wù)端Demo……瞬間頭大有沒有厕隧?

我們不管這些,拋開現(xiàn)象看本質(zhì)俄周,先分析下NIO是怎么工作的。

常見I/O模型對比

所有的系統(tǒng)I/O都分為兩個階段:等待就緒和操作髓迎。舉例來說峦朗,讀函數(shù),分為等待系統(tǒng)可讀和真正的讀排龄;同理波势,寫函數(shù)分為等待網(wǎng)卡可以寫和真正的寫。

需要說明的是等待就緒的阻塞是不使用CPU的橄维,是在“空等”尺铣;而真正的讀寫操作的阻塞是使用CPU的,真正在"干活"争舞,而且這個過程非沉莘蓿快,屬于memory copy竞川,帶寬通常在1GB/s級別以上店溢,可以理解為基本不耗時。

下圖是幾種常見I/O模型的對比:

以socket.read()為例子:

傳統(tǒng)的BIO里面socket.read()委乌,如果TCP RecvBuffer里沒有數(shù)據(jù)床牧,函數(shù)會一直阻塞,直到收到數(shù)據(jù)遭贸,返回讀到的數(shù)據(jù)戈咳。

對于NIO,如果TCP RecvBuffer有數(shù)據(jù)壕吹,就把數(shù)據(jù)從網(wǎng)卡讀到內(nèi)存著蛙,并且返回給用戶;反之則直接返回0算利,永遠不會阻塞册踩。

最新的AIO(Async I/O)里面會更進一步:不但等待就緒是非阻塞的,就連數(shù)據(jù)從網(wǎng)卡到內(nèi)存的過程也是異步的效拭。

換句話說暂吉,BIO里用戶最關(guān)心“我要讀”胖秒,NIO里用戶最關(guān)心"我可以讀了",在AIO模型里用戶更需要關(guān)注的是“讀完了”慕的。

NIO一個重要的特點是:socket主要的讀阎肝、寫、注冊和接收函數(shù)肮街,在等待就緒階段都是非阻塞的风题,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。

如何結(jié)合事件模型使用NIO同步非阻塞特性

回憶BIO模型嫉父,之所以需要多線程沛硅,是因為在進行I/O操作的時候,一是沒有辦法知道到底能不能寫绕辖、能不能讀摇肌,只能"傻等",即使通過各種估算仪际,算出來操作系統(tǒng)沒有能力進行讀寫围小,也沒法在socket.read()和socket.write()函數(shù)中返回,這兩個函數(shù)無法進行有效的中斷树碱。所以除了多開線程另起爐灶肯适,沒有好的辦法利用CPU。

NIO的讀寫函數(shù)可以立刻返回成榜,這就給了我們不開線程利用CPU的最好機會:如果一個連接不能讀寫(socket.read()返回0或者socket.write()返回0)框舔,我們可以把這件事記下來,記錄的方式通常是在Selector上注冊標記位伦连,然后切換到其它就緒的連接(channel)繼續(xù)進行讀寫雨饺。

下面具體看下如何利用事件模型單線程處理所有I/O請求:

NIO的主要事件有幾個:讀就緒、寫就緒惑淳、有新連接到來额港。

我們首先需要注冊當這幾個事件到來的時候所對應(yīng)的處理器。然后在合適的時機告訴事件選擇器:我對這個事件感興趣歧焦。對于寫操作移斩,就是寫不出去的時候?qū)懯录信d趣绢馍;對于讀操作,就是完成連接和系統(tǒng)沒有辦法承載新讀入的數(shù)據(jù)的時舰涌;對于accept,一般是服務(wù)器剛啟動的時候瓷耙;而對于connect朱躺,一般是connect失敗需要重連或者直接異步調(diào)用connect的時候。

其次宇弛,用一個死循環(huán)選擇就緒的事件,會執(zhí)行系統(tǒng)調(diào)用(Linux 2.6之前是select源请、poll枪芒,2.6之后是epoll谁尸,Windows是IOCP),還會阻塞的等待新事件的到來良蛮。新事件到來的時候,會在selector上注冊標記位,標示可讀瞒斩、可寫或者有連接到來。

注意胸囱,select是阻塞的瀑梗,無論是通過操作系統(tǒng)的通知(epoll)還是不停的輪詢(select,poll)谤职,這個函數(shù)是阻塞的亿鲜。所以你可以放心大膽地在一個while(true)里面調(diào)用這個函數(shù)而不用擔心CPU空轉(zhuǎn)。

所以我們的程序大概的模樣是:

? interface ChannelHandler{

? ? ? void channelReadable(Channel channel);

? ? ? void channelWritable(Channel channel);

? }

? class Channel{

? ? Socket socket;

? ? Event event;//讀饶套,寫或者連接

? }

? //IO線程主循環(huán):

? class IoThread extends Thread{

? public void run(){

? Channel channel;

? while(channel=Selector.select()){//選擇就緒的事件和對應(yīng)的連接

? ? ? if(channel.event==accept){

? ? ? ? registerNewChannelHandler(channel);//如果是新連接垒探,則注冊一個新的讀寫處理器

? ? ? }

? ? ? if(channel.event==write){

? ? ? ? getChannelHandler(channel).channelWritable(channel);//如果可以寫圾叼,則執(zhí)行寫事件

? ? ? }

? ? ? if(channel.event==read){

? ? ? ? ? getChannelHandler(channel).channelReadable(channel);//如果可以讀捺癞,則執(zhí)行讀事件

? ? ? }

? ? }

? }

? Map<Channel咖耘,ChannelHandler> handlerMap;//所有channel的對應(yīng)事件處理器

? }

這個程序很簡短,也是最簡單的Reactor模式:注冊所有感興趣的事件處理器版保,單線程輪詢選擇就緒事件夫否,執(zhí)行事件處理器。

優(yōu)化線程模型

由上面的示例我們大概可以總結(jié)出NIO是怎么解決掉線程的瓶頸并處理海量連接的:

NIO由原來的阻塞讀寫(占用線程)變成了單線程輪詢事件汞幢,找到可以進行讀寫的網(wǎng)絡(luò)描述符進行讀寫微谓。除了事件的輪詢是阻塞的(沒有可干的事情必須要阻塞),剩余的I/O操作都是純CPU操作仲智,沒有必要開啟多線程姻氨。

并且由于線程的節(jié)約,連接數(shù)大的時候因為線程切換帶來的問題也隨之解決前联,進而為處理海量連接提供了可能娶眷。

單線程處理I/O的效率確實非常高,沒有線程切換双谆,只是拼命的讀席揽、寫、選擇事件寸谜。但現(xiàn)在的服務(wù)器属桦,一般都是多核處理器他爸,如果能夠利用多核心進行I/O果善,無疑對效率會有更大的提高。

仔細分析一下我們需要的線程讨跟,其實主要包括以下幾種:

事件分發(fā)器鄙煤,單線程選擇就緒的事件。

I/O處理器凉馆,包括connect亡资、read、write等咳胃,這種純CPU操作旷太,一般開啟CPU核心個線程就可以销睁。

業(yè)務(wù)線程,在處理完I/O后睡毒,業(yè)務(wù)一般還會有自己的業(yè)務(wù)邏輯冗栗,有的還會有其他的阻塞I/O,如DB操作钠至,RPC等胎源。只要有阻塞,就需要單獨的線程宪卿。

Java的Selector對于Linux系統(tǒng)來說,有一個致命限制:同一個channel的select不能被并發(fā)的調(diào)用西疤。因此休溶,如果有多個I/O線程,必須保證:一個socket只能屬于一個IoThread管跺,而一個IoThread可以管理多個socket禾进。

另外連接的處理和讀寫的處理通常可以選擇分開艇拍,這樣對于海量連接的注冊和讀寫就可以分發(fā)宠纯。雖然read()和write()是比較高效無阻塞的函數(shù)邻奠,但畢竟會占用CPU,如果面對更高的并發(fā)則無能為力。

NIO在客戶端的魔力

通過上面的分析乖菱,可以看出NIO在服務(wù)端對于解放線程猴蹂,優(yōu)化I/O和處理海量連接方面,確實有自己的用武之地珍逸。那么在客戶端上聋溜,NIO又有什么使用場景呢?

常見的客戶端BIO+連接池模型,可以建立n個連接撮躁,然后當某一個連接被I/O占用的時候,可以使用其他連接來提高性能缨称。

但多線程的模型面臨和服務(wù)端相同的問題:如果指望增加連接數(shù)來提高性能,則連接數(shù)又受制于線程數(shù)器净、線程很貴当凡、無法建立很多線程沿量,則性能遇到瓶頸。

每連接順序請求的Redis

對于Redis來說朴则,由于服務(wù)端是全局串行的乌妒,能夠保證同一連接的所有請求與返回順序一致。這樣可以使用單線程+隊列古掏,把請求數(shù)據(jù)緩沖侦啸。然后pipeline發(fā)送,返回future夏漱,然后channel可讀時顶捷,直接在隊列中把future取回來屎篱,done()就可以了交播。

偽代碼如下:

class RedisClient Implements ChannelHandler{

private BlockingQueue CmdQueue;

private EventLoop eventLoop;

private Channel channel;

class Cmd{

? String cmd;

? Future result;

}

public Future get(String key){

? Cmd cmd= new Cmd(key);

? queue.offer(cmd);

? eventLoop.submit(new Runnable(){

? ? ? ? List list = new ArrayList();

? ? ? ? queue.drainTo(list);

? ? ? ? if(channel.isWritable()){

? ? ? ? channel.writeAndFlush(list);

? ? ? ? }

? });

}

public void ChannelReadFinish(Channel channel,Buffer Buffer){

? ? List result = handleBuffer();//處理數(shù)據(jù)

? ? //從cmdQueue取出future缺厉,并設(shè)值,future.done();

}

public void ChannelWritable(Channel channel){

? channel.flush();

}

}

這樣做命爬,能夠充分的利用pipeline來提高I/O能力辐脖,同時獲取異步處理能力嗜价。

多連接短連接的HttpClient

類似于競對抓取的項目,往往需要建立無數(shù)的HTTP短連接家淤,然后抓取瑟由,然后銷毀,當需要單機抓取上千網(wǎng)站線程數(shù)又受制的時候绿鸣,怎么保證性能呢?

何不嘗試NIO暂氯,單線程進行連接、寫擎厢、讀操作辣吃?如果連接神得、讀、寫操作系統(tǒng)沒有能力處理宵蕉,簡單的注冊一個事件节榜,等待下次循環(huán)就好了。

如何存儲不同的請求/響應(yīng)呢稼稿?由于http是無狀態(tài)沒有版本的協(xié)議,又沒有辦法使用隊列敞恋,好像辦法不多是越。比較笨的辦法是對于不同的socket,直接存儲socket的引用作為map的key浦徊。

常見的RPC框架天梧,如Thrift呢岗,Dubbo

這種框架內(nèi)部一般維護了請求的協(xié)議和請求號,可以維護一個以請求號為key悉尾,結(jié)果的result為future的map挫酿,結(jié)合NIO+長連接,獲取非常不錯的性能惫霸。

NIO高級主題

Proactor與Reactor

一般情況下葱弟,I/O 復(fù)用機制需要事件分發(fā)器(event dispatcher)芝加。 事件分發(fā)器的作用,即將那些讀寫事件源分發(fā)給各讀寫事件的處理者将塑,就像送快遞的在樓下喊: 誰誰誰的快遞到了制市, 快來拿吧弊予!開發(fā)人員在開始的時候需要在分發(fā)器那里注冊感興趣的事件,并提供相應(yīng)的處理者(event handler)责鳍,或者是回調(diào)函數(shù)兽间;事件分發(fā)器在適當?shù)臅r候,會將請求的事件分發(fā)給這些handler或者回調(diào)函數(shù)恤溶。

涉及到事件分發(fā)器的兩種模式稱為:Reactor和Proactor帜羊。 Reactor模式是基于同步I/O的讼育,而Proactor模式是和異步I/O相關(guān)的。在Reactor模式中饥瓷,事件分發(fā)器等待某個事件或者可應(yīng)用或個操作的狀態(tài)發(fā)生(比如文件描述符可讀寫痹籍,或者是socket可讀寫),事件分發(fā)器就把這個事件傳給事先注冊的事件處理函數(shù)或者回調(diào)函數(shù)刺洒,由后者來做實際的讀寫操作逆航。

而在Proactor模式中渔肩,事件處理者(或者代由事件分發(fā)器發(fā)起)直接發(fā)起一個異步讀寫操作(相當于請求),而實際的工作是由操作系統(tǒng)來完成的抹剩。發(fā)起時蓉坎,需要提供的參數(shù)包括用于存放讀到數(shù)據(jù)的緩存區(qū)蛉艾、讀的數(shù)據(jù)大小或用于存放外發(fā)數(shù)據(jù)的緩存區(qū)衷敌,以及這個請求完后的回調(diào)函數(shù)等信息缴罗。事件分發(fā)器得知了這個請求祭埂,它默默等待這個請求的完成,然后轉(zhuǎn)發(fā)完成事件給相應(yīng)的事件處理者或者回調(diào)舌界。舉例來說航罗,在Windows上事件處理者投遞了一個異步IO操作(稱為overlapped技術(shù))粥血,事件分發(fā)器等IO Complete事件完成。這種異步模式的典型實現(xiàn)是基于操作系統(tǒng)底層異步API的趾娃,所以我們可稱之為“系統(tǒng)級別”的或者“真正意義上”的異步缔御,因為具體的讀寫是由操作系統(tǒng)代勞的。

舉個例子耕突,將有助于理解Reactor與Proactor二者的差異眷茁,以讀操作為例(寫操作類似)。

在Reactor中實現(xiàn)讀

注冊讀就緒事件和相應(yīng)的事件處理器培遵。

事件分發(fā)器等待事件登刺。

事件到來纸俭,激活分發(fā)器,分發(fā)器調(diào)用事件對應(yīng)的處理器郎楼。

事件處理器完成實際的讀操作,處理讀到的數(shù)據(jù),注冊新的事件蛉迹,然后返還控制權(quán)北救。

在Proactor中實現(xiàn)讀:

處理器發(fā)起異步讀操作(注意:操作系統(tǒng)必須支持異步IO)。在這種情況下托启,處理器無視IO就緒事件攘宙,它關(guān)注的是完成事件蹭劈。

事件分發(fā)器等待操作完成事件。

在分發(fā)器等待過程中多矮,操作系統(tǒng)利用并行的內(nèi)核線程執(zhí)行實際的讀操作哈打,并將結(jié)果數(shù)據(jù)存入用戶自定義緩沖區(qū)料仗,最后通知事件分發(fā)器讀操作完成。

事件分發(fā)器呼喚處理器淹仑。

事件處理器處理用戶自定義緩沖區(qū)中的數(shù)據(jù)肺孵,然后啟動一個新的異步操作平窘,并將控制權(quán)返回事件分發(fā)器。

可以看出是鬼,兩個模式的相同點,都是對某個I/O事件的事件通知(即告訴某個模塊李剖,這個I/O操作可以進行或已經(jīng)完成)囤耳。在結(jié)構(gòu)上充择,兩者也有相同點:事件分發(fā)器負責(zé)提交IO操作(異步)、查詢設(shè)備是否可操作(同步)宰僧,然后當條件滿足時观挎,就回調(diào)handler嘁捷;不同點在于,異步情況下(Proactor)谜疤,當回調(diào)handler時现诀,表示I/O操作已經(jīng)完成仔沿;同步情況下(Reactor),回調(diào)handler時绵跷,表示I/O設(shè)備可以進行某個操作(can read 或 can write)成福。

下面奴艾,我們將嘗試應(yīng)對為Proactor和Reactor模式建立可移植框架的挑戰(zhàn)。在改進方案中像啼,我們將Reactor原來位于事件處理器內(nèi)的Read/Write操作移至分發(fā)器(不妨將這個思路稱為“模擬異步”)忽冻,以此尋求將Reactor多路同步I/O轉(zhuǎn)化為模擬異步I/O。以讀操作為例子蜜猾,改進過程如下:

注冊讀就緒事件和相應(yīng)的事件處理器振诬。并為分發(fā)器提供數(shù)據(jù)緩沖區(qū)地址赶么,需要讀取數(shù)據(jù)量等信息脊串。

分發(fā)器等待事件(如在select()上等待)琼锋。

事件到來,激活分發(fā)器怖侦。分發(fā)器執(zhí)行一個非阻塞讀操作(它有完成這個操作所需的全部信息)谜叹,最后調(diào)用對應(yīng)處理器荷腊。

事件處理器處理用戶自定義緩沖區(qū)的數(shù)據(jù),注冊新的事件(當然同樣要給出數(shù)據(jù)緩沖區(qū)地址猜年,需要讀取的數(shù)據(jù)量等信息)乔外,最后將控制權(quán)返還分發(fā)器一罩。

如我們所見擒抛,通過對多路I/O模式功能結(jié)構(gòu)的改造补疑,可將Reactor轉(zhuǎn)化為Proactor模式莲组。改造前后暖夭,模型實際完成的工作量沒有增加迈着,只不過參與者間對工作職責(zé)稍加調(diào)換。沒有工作量的改變咬清,自然不會造成性能的削弱奴潘。對如下各步驟的比較画髓,可以證明工作量的恒定:

標準/典型的Reactor:

步驟1:等待事件到來(Reactor負責(zé))。

步驟2:將讀就緒事件分發(fā)給用戶定義的處理器(Reactor負責(zé))夺谁。

步驟3:讀數(shù)據(jù)(用戶處理器負責(zé))予权。

步驟4:處理數(shù)據(jù)(用戶處理器負責(zé))扫腺。

改進實現(xiàn)的模擬Proactor:

步驟1:等待事件到來(Proactor負責(zé))村象。

步驟2:得到讀就緒事件厚者,執(zhí)行讀數(shù)據(jù)(現(xiàn)在由Proactor負責(zé))。

步驟3:將讀完成事件分發(fā)給用戶處理器(Proactor負責(zé))账忘。

步驟4:處理數(shù)據(jù)(用戶處理器負責(zé))鳖擒。

對于不提供異步I/O API的操作系統(tǒng)來說,這種辦法可以隱藏Socket API的交互細節(jié)戳稽,從而對外暴露一個完整的異步接口惊奇。借此播赁,我們就可以進一步構(gòu)建完全可移植的,平臺無關(guān)的乓序,有通用對外接口的解決方案。

代碼示例如下:

interface ChannelHandler{

? ? ? void channelReadComplate(Channel channel房维,byte[] data);

? ? ? void channelWritable(Channel channel);

? }

? class Channel{

? ? Socket socket;

? ? Event event;//讀咙俩,寫或者連接

? }

? //IO線程主循環(huán):

? class IoThread extends Thread{

? public void run(){

? Channel channel;

? while(channel=Selector.select()){//選擇就緒的事件和對應(yīng)的連接

? ? ? if(channel.event==accept){

? ? ? ? registerNewChannelHandler(channel);//如果是新連接阿趁,則注冊一個新的讀寫處理器

? ? ? ? Selector.interested(read);

? ? ? }

? ? ? if(channel.event==write){

? ? ? ? getChannelHandler(channel).channelWritable(channel);//如果可以寫脖阵,則執(zhí)行寫事件

? ? ? }

? ? ? if(channel.event==read){

? ? ? ? ? byte[] data = channel.read();

? ? ? ? ? if(channel.read()==0)//沒有讀到數(shù)據(jù),表示本次數(shù)據(jù)讀完了

? ? ? ? ? {

? ? ? ? ? getChannelHandler(channel).channelReadComplate(channel呜呐,data;//處理讀完成事件

? ? ? ? ? }

? ? ? ? ? if(過載保護){

? ? ? ? ? Selector.interested(read);

? ? ? ? ? }

? ? ? }

? ? }

? ? }

? Map<Channel蘑辑,ChannelHandler> handlerMap;//所有channel的對應(yīng)事件處理器

? }

Selector.wakeup()

主要作用

解除阻塞在Selector.select()/select(long)上的線程,立即返回坠宴。

兩次成功的select之間多次調(diào)用wakeup等價于一次調(diào)用洋魂。

如果當前沒有阻塞在select上,則本次wakeup調(diào)用將作用于下一次select——“記憶”作用。

為什么要喚醒副砍?

注冊了新的channel或者事件衔肢。

channel關(guān)閉,取消注冊址晕。

優(yōu)先級更高的事件觸發(fā)(如定時器事件)膀懈,希望及時處理。

原理

Linux上利用pipe調(diào)用創(chuàng)建一個管道启搂,Windows上則是一個loopback的tcp連接。這是因為win32的管道無法加入select的fd set刘陶,將管道或者TCP連接加入select fd set胳赌。

wakeup往管道或者連接寫入一個字節(jié),阻塞的select因為有I/O事件就緒匙隔,立即返回疑苫。可見纷责,wakeup的調(diào)用開銷不可忽視捍掺。

Buffer的選擇

通常情況下,操作系統(tǒng)的一次寫操作分為兩步:

將數(shù)據(jù)從用戶空間拷貝到系統(tǒng)空間再膳。

從系統(tǒng)空間往網(wǎng)卡寫挺勿。同理,讀操作也分為兩步:

① 將數(shù)據(jù)從網(wǎng)卡拷貝到系統(tǒng)空間喂柒;

② 將數(shù)據(jù)從系統(tǒng)空間拷貝到用戶空間不瓶。

對于NIO來說,緩存的使用可以使用DirectByteBuffer和HeapByteBuffer灾杰。如果使用了DirectByteBuffer蚊丐,一般來說可以減少一次系統(tǒng)空間到用戶空間的拷貝。但Buffer創(chuàng)建和銷毀的成本更高艳吠,更不宜維護麦备,通常會用內(nèi)存池來提高性能。

如果數(shù)據(jù)量比較小的中小應(yīng)用情況下昭娩,可以考慮使用heapBuffer泥兰;反之可以用directBuffer。

NIO存在的問題

使用NIO != 高性能题禀,當連接數(shù)<1000鞋诗,并發(fā)程度不高或者局域網(wǎng)環(huán)境下NIO并沒有顯著的性能優(yōu)勢。

NIO并沒有完全屏蔽平臺差異迈嘹,它仍然是基于各個操作系統(tǒng)的I/O系統(tǒng)實現(xiàn)的削彬,差異仍然存在全庸。使用NIO做網(wǎng)絡(luò)編程構(gòu)建事件驅(qū)動模型并不容易,陷阱重重融痛。

推薦大家使用成熟的NIO框架壶笼,如Netty,MINA等雁刷。解決了很多NIO的陷阱覆劈,并屏蔽了操作系統(tǒng)的差異,有較好的性能和編程模型沛励。

總結(jié)

最后總結(jié)一下到底NIO給我們帶來了些什么:

事件驅(qū)動模型

避免多線程

單線程處理多任務(wù)

非阻塞I/O责语,I/O讀寫不再阻塞,而是返回0

基于block的傳輸目派,通常比基于流的傳輸更高效

更高級的IO函數(shù)坤候,zero-copy

IO多路復(fù)用大大提高了Java網(wǎng)絡(luò)應(yīng)用的可伸縮性和實用性

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市企蹭,隨后出現(xiàn)的幾起案子白筹,更是在濱河造成了極大的恐慌,老刑警劉巖谅摄,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件徒河,死亡現(xiàn)場離奇詭異,居然都是意外死亡送漠,警方通過查閱死者的電腦和手機顽照,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來螺男,“玉大人棒厘,你說我怎么就攤上這事纵穿∠滤恚” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵谓媒,是天一觀的道長淆院。 經(jīng)常有香客問我,道長句惯,這世上最難降的妖魔是什么土辩? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮抢野,結(jié)果婚禮上拷淘,老公的妹妹穿的比我還像新娘。我一直安慰自己指孤,他們只是感情好启涯,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布贬堵。 她就那樣靜靜地躺著,像睡著了一般结洼。 火紅的嫁衣襯著肌膚如雪黎做。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天松忍,我揣著相機與錄音蒸殿,去河邊找鬼。 笑死鸣峭,一個胖子當著我的面吹牛宏所,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播叽掘,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼楣铁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了更扁?” 一聲冷哼從身側(cè)響起盖腕,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浓镜,沒想到半個月后溃列,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡膛薛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年听隐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哄啄。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡雅任,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咨跌,到底是詐尸還是另有隱情沪么,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布锌半,位于F島的核電站禽车,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏刊殉。R本人自食惡果不足惜殉摔,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望记焊。 院中可真熱鬧逸月,春花似錦、人聲如沸遍膜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肛响,卻和暖如春岭粤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背特笋。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工剃浇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人猎物。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓虎囚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蔫磨。 傳聞我的和親對象是個殘疾皇子淘讥,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容