Java NIO 淺析

在閱讀本文前,強烈建議閱讀一下:《Java NIO AIO 基本概念

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

回憶BIO模型,之所以需要多線程,是因爲在進行I/O操作的時候,一是沒有辦法知道到底能不能寫、能不能讀,只能"傻等",即使通過各種估算,算出來操作系統沒有能力進行讀寫,也沒法在socket.read()和socket.write()函數中返回,這兩個函數無法進行有效的中斷。所以除了多開線程另起爐竈,沒有好的辦法利用CPU。

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

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

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

我們首先需要註冊當這幾個事件到來的時候所對應的處理器。然後在合適的時機告訴事件選擇器:我對這個事件感興趣。對於寫操作,就是寫不出去的時候對寫事件感興趣;對於讀操作,就是完成連接和系統沒有辦法承載新讀入的數據的時;對於accept,一般是服務器剛啓動的時候;而對於connect,一般是connect失敗需要重連或者直接異步調用connect的時候。

其次,用一個死循環選擇就緒的事件,會執行系統調用(Linux 2.6之前是select、poll,2.6之後是epoll,Windows是IOCP),還會阻塞的等待新事件的到來。新事件到來的時候,會在selector上註冊標記位,標示可讀、可寫或者有連接到來。

注意,select是阻塞的,無論是通過操作系統的通知(epoll)還是不停的輪詢(select,poll),這個函數是阻塞的。所以你可以放心大膽地在一個while(true)裏面調用這個函數而不用擔心CPU空轉。

優化線程模型

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

NIO由原來的阻塞讀寫(佔用線程)變成了單線程輪詢事件,找到可以進行讀寫的網絡描述符進行讀寫。除了事件的輪詢是阻塞的(沒有可乾的事情必須要阻塞),剩餘的I/O操作都是純CPU操作,沒有必要開啓多線程。

並且由於線程的節約,連接數大的時候因爲線程切換帶來的問題也隨之解決,進而爲處理海量連接提供了可能。

單線程處理I/O的效率確實非常高,沒有線程切換,只是拼命的讀、寫、選擇事件。但現在的服務器,一般都是多核處理器,如果能夠利用多核心進行I/O,無疑對效率會有更大的提高。

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

  • 事件分發器,單線程選擇就緒的事件。
  • I/O處理器,包括connect、read、write等,這種純CPU操作,一般開啓CPU核心個線程就可以。
  • 業務線程,在處理完I/O後,業務一般還會有自己的業務邏輯,有的還會有其他的阻塞I/O,如DB操作,RPC等。只要有阻塞,就需要單獨的線程。

Java的Selector對於Linux系統來說,有一個致命限制:同一個channel的select不能被併發的調用。因此,如果有多個I/O線程,必須保證:一個socket只能屬於一個IoThread,而一個IoThread可以管理多個socket。

另外連接的處理和讀寫的處理通常可以選擇分開,這樣對於海量連接的註冊和讀寫就可以分發。雖然read()和write()是比較高效無阻塞的函數,但畢竟會佔用CPU,如果面對更高的併發則無能爲力。
在這裏插入圖片描述

Proactor與Reactor

一般情況下,I/O 複用機制需要事件分發器(event dispatcher)。 事件分發器的作用,即將那些讀寫事件源分發給各讀寫事件的處理者,就像送快遞的在樓下喊: 誰誰誰的快遞到了, 快來拿吧!開發人員在開始的時候需要在分發器那裏註冊感興趣的事件,並提供相應的處理者(event handler),或者是回調函數;事件分發器在適當的時候,會將請求的事件分發給這些handler或者回調函數。

涉及到事件分發器的兩種模式稱爲:Reactor和Proactor。 Reactor模式是基於同步I/O的,而Proactor模式是和異步I/O相關的。在Reactor模式中,事件分發器等待某個事件或者可應用或個操作的狀態發生(比如文件描述符可讀寫,或者是socket可讀寫),事件分發器就把這個事件傳給事先註冊的事件處理函數或者回調函數,由後者來做實際的讀寫操作。

而在Proactor模式中,事件處理者(或者代由事件分發器發起)直接發起一個異步讀寫操作(相當於請求),而實際的工作是由操作系統來完成的。發起時,需要提供的參數包括用於存放讀到數據的緩存區、讀的數據大小或用於存放外發數據的緩存區,以及這個請求完後的回調函數等信息。事件分發器得知了這個請求,它默默等待這個請求的完成,然後轉發完成事件給相應的事件處理者或者回調。舉例來說,在Windows上事件處理者投遞了一個異步IO操作(稱爲overlapped技術),事件分發器等IO Complete事件完成。這種異步模式的典型實現是基於操作系統底層異步API的,所以我們可稱之爲“系統級別”的或者“真正意義上”的異步,因爲具體的讀寫是由操作系統代勞的。

本文整理自《Java NIO 淺析

個人微信公衆號:
這裏寫圖片描述

作者:jiankunking 出處:http://blog.csdn.net/jiankunking

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章