徹底搞懂NIO效率高的原理

前言

這篇文章讀不懂的沒關系厢漩,可以先收藏一下待错。筆者準備介紹完epoll和NIO等知識點褐澎,然后寫一篇Java網(wǎng)絡IO模型的介紹会钝,這樣可以使Java網(wǎng)絡IO的知識體系更加地完整和嚴謹。初學者也可以等看完IO模型介紹的博客之后工三,再回頭看這些博客迁酸,會更加有收獲。

NIO相比BIO的優(yōu)勢

NIO(Non-blocking I/O俭正,在Java領域奸鬓,也稱為New I/O),是一種同步非阻塞的I/O模型掸读,也是I/O多路復用的基礎串远,已經(jīng)被越來越多地應用到大型應用服務器,成為解決高并發(fā)與大量連接儿惫、I/O處理問題的有效方式澡罚。

bio與nio

面向流與面向緩沖

Java NIO和BIO之間第一個最大的區(qū)別是,BIO是面向流的肾请,NIO是面向緩沖區(qū)的留搔。 JavaIO面向流意味著每次從流中讀一個或多個字節(jié),直至讀取所有字節(jié)铛铁,它們沒有被緩存在任何地方隔显。此外,它不能前后移動流中的數(shù)據(jù)饵逐。如果需要前后移動從流中讀取的數(shù)據(jù)括眠,需要先將它緩存到一個緩沖區(qū)。Java NIO的緩沖讀取方法略有不同梳毙。數(shù)據(jù)讀取到一個緩沖區(qū)哺窄,需要時可在緩沖區(qū)中前后移動捐下。這就增加了處理過程中的靈活性账锹。但是,還需要檢查是否該緩沖區(qū)中包含所有需要處理的數(shù)據(jù)坷襟。而且奸柬,需確保當更多的數(shù)據(jù)讀入緩沖區(qū)時,不要覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)婴程。

有關面向緩沖讀取數(shù)據(jù)的示例和注意點廓奕,可以點擊查看

阻塞IO與非阻塞IO

Java IO的各種流是阻塞的。這意味著,當一個線程調(diào)用read() 或write()時桌粉,該線程被阻塞蒸绩,直到有數(shù)據(jù)被讀取或者數(shù)據(jù)寫入。該線程在阻塞期間不能做其他事情铃肯。而Java NIO的非阻塞模式患亿,如果通道沒有東西可讀,或不可寫押逼,讀寫函數(shù)馬上返回步藕,而不會阻塞,這個線程可以去做別的事情挑格。 線程通常將非阻塞IO的空閑時間用于在其它通道上執(zhí)行IO操作咙冗,所以一個單獨的線程可以管理多個輸入和輸出通道(channel),即IO多路復用的原理漂彤。

零拷貝

在傳統(tǒng)的文件IO操作中雾消,我們都是調(diào)用操作系統(tǒng)提供的底層標準IO系統(tǒng)調(diào)用函數(shù)read()、write() 挫望,此時調(diào)用此函數(shù)的進程(在JAVA中即java進程)由當前的用戶態(tài)切換到內(nèi)核態(tài)仪或,然后OS的內(nèi)核代碼負責將相應的文件數(shù)據(jù)讀取到內(nèi)核的IO緩沖區(qū),然后再把數(shù)據(jù)從內(nèi)核IO緩沖區(qū)拷貝到進程的私有地址空間中去士骤,這樣便完成了一次IO操作范删。

IO

而NIO的零拷貝與傳統(tǒng)的文件IO操作最大的不同之處就在于它雖然也是要從磁盤讀取數(shù)據(jù),但是它并不需要將數(shù)據(jù)讀取到OS內(nèi)核緩沖區(qū)拷肌,而是直接將進程的用戶私有地址空間中的一部分區(qū)域與文件對象建立起映射關系到旦,這樣直接從內(nèi)存中讀寫文件,速度大幅度提升巨缘。

NIO

詳細的解析添忘,之后會有單獨的博客進行講解

NIO的核心部分

Java NIO主要由以下三個核心部分組成:

  • Channel
  • Buffer
  • Selector

Channel

基本上,所有的IO在NIO中都從一個Channel開始若锁。數(shù)據(jù)可以從Channel讀到Buffer中搁骑,也可以從Buffer寫到Channel中。這里有個圖示:

channel與buffer

Channel和Buffer有好幾種類型又固。下面是Java NIO中的一些主要Channel的實現(xiàn):

  • FileChannel(file)
  • DatagramChannel(UDP)
  • SocketChannel(TCP)
  • ServerSocketChannel(TCP)

這些通道涵蓋了UDP和TCP網(wǎng)絡IO以及文件IO仲器。

最后兩個channel的關系。通過 ServerSocketChannel.accept() 方法監(jiān)聽新進來的連接仰冠。當 accept()方法返回的時候,它返回一個包含新進來的連接的 SocketChannel乏冀。因此, accept()方法會一直阻塞到有新連接到達。通常不會僅僅只監(jiān)聽一個連接,在while循環(huán)中調(diào)用 accept()方法.


//打開 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();
    //do something with socketChannel...
}
//關閉ServerSocketChannel
serverSocketChannel.close();

Buffer

緩沖區(qū)本質(zhì)上是一塊可以寫入數(shù)據(jù)洋只,然后可以從中讀取數(shù)據(jù)的內(nèi)存辆沦。這塊內(nèi)存被包裝成NIO Buffer對象昼捍,并提供了一組方法,用來方便的訪問該塊內(nèi)存肢扯。

Java NIO里關鍵的Buffer實現(xiàn):

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

這些Buffer覆蓋了你能通過IO發(fā)送的基本數(shù)據(jù)類型:byte妒茬、short、int蔚晨、long郊闯、float、double和char蛛株。

為了理解Buffer的工作原理团赁,需要熟悉它的三個屬性:

  • capacity
  • position
  • limit

position和limit的含義取決于Buffer處在讀模式還是寫模式。不管Buffer處在什么模式谨履,capacity的含義總是一樣的欢摄。

buffer模型

capacity

作為一個內(nèi)存塊,Buffer有個固定的最大值笋粟,就是capacity怀挠。Buffer只能寫capacity個byte、long害捕、char等類型绿淋。一旦Buffer滿了,需要將其清空(通過讀數(shù)據(jù)或者清除數(shù)據(jù))才能繼續(xù)寫數(shù)據(jù)往里寫數(shù)據(jù)尝盼。

position

當寫數(shù)據(jù)到Buffer中時吞滞,position表示當前的位置。初始的position值為0盾沫。當一個byte裁赠、long等數(shù)據(jù)寫到Buffer后, position會向前移動到下一個可插入數(shù)據(jù)的Buffer單元赴精。position最大可為capacity – 1.

當讀取數(shù)據(jù)時佩捞,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式蕾哟,position會被重置為0一忱。 當從Buffer的position處讀取數(shù)據(jù)時,position向前移動到下一個可讀的位置谭确。

limit

在寫模式下帘营,Buffer的limit表示最多能往Buffer里寫多少數(shù)據(jù)。 寫模式下琼富,limit等于capacity仪吧。

當切換Buffer到讀模式時, limit表示你最多能讀到多少數(shù)據(jù)鞠眉。因此薯鼠,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值械蹋。

Selector

Selector允許單線程處理多個 Channel出皇。如果你的應用打開了多個連接(通道),但每個連接的流量都很低哗戈,使用Selector就會很方便郊艘。例如,在一個聊天服務器中唯咬。

這是在一個單線程中使用一個Selector處理3個Channel的圖示:

Selector

要使用Selector纱注,得向Selector注冊Channel,然后調(diào)用它的select()方法胆胰。這個方法會一直阻塞到某個注冊的通道有事件就緒狞贱。一旦這個方法返回,線程就可以處理這些事件蜀涨,事件例如有新連接進來瞎嬉,數(shù)據(jù)接收等。

NIO與epoll的關系

Java NIO根據(jù)操作系統(tǒng)不同厚柳, 針對NIO中的Selector有不同的實現(xiàn):

  • macosx:KQueueSelectorProvider
  • solaris:DevPollSelectorProvider
  • Linux:EPollSelectorProvider (Linux kernels >= 2.6)或PollSelectorProvider
  • windows:WindowsSelectorProvider

所以不需要特別指定氧枣,Oracle JDK會自動選擇合適的Selector。
如果想設置特定的Selector别垮,可以設置屬性便监,例如:
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider

JDK在Linux已經(jīng)默認使用epoll方式,但是JDK的epoll采用的是水平觸發(fā)碳想,所以Netty自4.0.16起, Netty為Linux通過JNI的方式提供了native socket transport茬贵。Netty重新實現(xiàn)了epoll機制,

  1. 采用邊緣觸發(fā)方式
  2. netty epoll transport暴露了更多的nio沒有的配置參數(shù)移袍,如 TCP_CORK, SO_REUSEADDR等等解藻。
  3. C代碼,更少GC葡盗,更少synchronized

使用native socket transport的方法很簡單螟左,只需將相應的類替換即可。

NioEventLoopGroup → EpollEventLoopGroup
NioEventLoop → EpollEventLoop
NioServerSocketChannel → EpollServerSocketChannel
NioSocketChannel → EpollSocketChannel  

有關epoll的詳細講解觅够,可以點擊查看

NIO處理消息的核心思路

結合示例代碼胶背,總結NIO的核心思路:

  1. NIO 模型中通常會有兩個線程,每個線程綁定一個輪詢器 selector 喘先,在上面例子中serverSelector負責輪詢是否有新的連接钳吟,clientSelector負責輪詢連接是否有數(shù)據(jù)可讀
  2. 服務端監(jiān)測到新的連接之后,不再創(chuàng)建一個新的線程窘拯,而是直接將新連接綁定到clientSelector上红且,這樣就不用BIO模型中1w 個while循環(huán)在阻塞坝茎,參見(1)
  3. clientSelector被一個 while 死循環(huán)包裹著,如果在某一時刻有多條連接有數(shù)據(jù)可讀暇番,那么通過clientSelector.select(1)方法可以輪詢出來嗤放,進而批量處理,參見(2)
  4. 數(shù)據(jù)的讀寫面向 Buffer壁酬,參見(3)

NIO的示例代碼


public class NIOServer {
    public static void main(String[] args) throws IOException {
        Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();

        new Thread(() -> {
            try {
                // 對應IO編程中服務端啟動
                ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                listenerChannel.socket().bind(new InetSocketAddress(8000));
                listenerChannel.configureBlocking(false);
                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

                while (true) {
                    // 監(jiān)測是否有新的連接次酌,這里的1指的是阻塞的時間為 1ms
                    if (serverSelector.select(1) > 0) {
                        Set<SelectionKey> set = serverSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isAcceptable()) {
                                try {
                                    // (1) 每來一個新連接,不需要創(chuàng)建一個線程舆乔,而是直接注冊到clientSelector
                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                    clientChannel.configureBlocking(false);
                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                } finally {
                                    keyIterator.remove();
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }

        }).start();


        new Thread(() -> {
            try {
                while (true) {
                    // (2) 批量輪詢是否有哪些連接有數(shù)據(jù)可讀岳服,這里的1指的是阻塞的時間為 1ms
                    if (clientSelector.select(1) > 0) {
                        Set<SelectionKey> set = clientSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isReadable()) {
                                try {
                                    SocketChannel clientChannel = (SocketChannel) key.channel();
                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                    // (3) 面向 Buffer
                                    clientChannel.read(byteBuffer);
                                    byteBuffer.flip();
                                    System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                            .toString());
                                } finally {
                                    keyIterator.remove();
                                    key.interestOps(SelectionKey.OP_READ);
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }
        }).start();


    }
}

哎呀,如果我的名片丟了希俩。微信搜索“全菜工程師小輝”吊宋,依然可以找到我
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市斜纪,隨后出現(xiàn)的幾起案子贫母,更是在濱河造成了極大的恐慌,老刑警劉巖盒刚,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腺劣,死亡現(xiàn)場離奇詭異,居然都是意外死亡因块,警方通過查閱死者的電腦和手機橘原,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涡上,“玉大人趾断,你說我怎么就攤上這事》岳ⅲ” “怎么了芋酌?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長雁佳。 經(jīng)常有香客問我脐帝,道長,這世上最難降的妖魔是什么糖权? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任堵腹,我火速辦了婚禮,結果婚禮上星澳,老公的妹妹穿的比我還像新娘疚顷。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布腿堤。 她就那樣靜靜地躺著阀坏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪释液。 梳的紋絲不亂的頭發(fā)上全释,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天装处,我揣著相機與錄音误债,去河邊找鬼。 笑死妄迁,一個胖子當著我的面吹牛寝蹈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播登淘,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼箫老,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了黔州?” 一聲冷哼從身側響起耍鬓,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎流妻,沒想到半個月后牲蜀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡绅这,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年涣达,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片证薇。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡度苔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出浑度,到底是詐尸還是另有隱情寇窑,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布箩张,位于F島的核電站甩骏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏伏钠。R本人自食惡果不足惜横漏,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望熟掂。 院中可真熱鬧缎浇,春花似錦、人聲如沸赴肚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至指厌,卻和暖如春查牌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背群井。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工赴恨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人箕憾。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓牡借,卻偏偏與公主長得像,于是被迫代替她去往敵國和親袭异。 傳聞我的和親對象是個殘疾皇子钠龙,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

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