Java NIO Selector實現原理

咱們先看看傳統I/O模型工做模式:java

每條socket鏈路都由一個單獨的線程負責處理,這對於大容量、高併發的應用程序來講,使用上千萬個線程來處理請求幾乎是不可能實現的。git

多路複用IO模型的工做模式:github

在多路複用IO模型中,只須要使用一個線程就能夠管理多個socket,並負責處理對應I/O事件。這對於構建高併發、大容量的服務端應用程序來講是很是有意義。安全

從圖中咱們能夠看出,多路複用IO模型中,使用了一個Selector對象來管理多個通道,這是實現單個線程能夠高效地處理多個socket上I/O事件的關鍵所在。下面開始介紹Java NIO中Selector所實現的功能和原理。併發

Selector簡介

簡單來講,Selector就是SelectableChannel對象的多路複用器。一般調用Selector類的靜態方法open來建立一個選擇器對象,該方法使用系統默認SelectorProvider對象的openSelector方法來建立新的選擇器。固然,還能夠自定義實現SelectorProvider並重寫openSelector方法來建立自定義選擇器。socket

一個Channel對象註冊到選擇器以後,會返回一個SelectionKey對象,這個SelectionKey對象表明這個Channel和它註冊的Selector間的關係。SelectionKey中維護着兩個很重要的屬性:interestOps、readyOps,並經過這兩個屬性管理通道上註冊的事件。interestOps中保存了咱們但願Selector監聽Channel的哪些事件,在Selector每次作select操做時,若發現該Channel有咱們所監聽的事件發生時,就會將感興趣的監聽事件設置到readyOps中,這樣咱們能夠根據事件的發生執行相應的I/O操做。ide

Selector的重要屬性

每一個選擇器中管理着三個SelectionKey集合:高併發

  • keys:該集合中保存了全部註冊到當前選擇器上的通道的SelectionKey對象;
  • selectedKeys:該集合中保存了上一次Selector選擇期間,發生了就緒事件的通道的SelectionKey對象集合,它始終是keys的子集。
  • cancelledKeys:該集合保存了已經被取消但其關聯的通道還未被註銷的SelectionKey對象集合,它始終是keys的子集。

初始化Selector對象時,這三個集合都爲空。當咱們調用Channel的register方法將通道註冊到選擇器時,一個SelectionKey對象會被加入到keys集合;調用通道的Close方法或直接調用選擇器的cancel方法則會將一個SelectionKey對象添加到cancelledKeys集合,選擇器下一次作選擇操做時,將會清空cancelledKeys中保存的選擇鍵,並從keys集合中刪除;選擇器作選擇操做時,具備就緒事件的SelectionKey對象會被加入到selectedKeys集合中。編碼

同時,每一個Selector中還維護了publicKeys和publicSelectedKeys兩個視圖,供客戶端使用。publicKeys是keys的視圖,調用Selector的keys()方法返回的就是publicKeys,publicKeys不支持添加和刪除操做;publicSelectedKeys是selectedKeys的視圖,它是是個不可增加的集合,即不支持add操做,但支持remove操做,調用publicSelectedKeys集合的remove操做實際是從selectedKeys中刪除一個SelectionKey對象。咱們能夠調用Selector的selectedKeys()方法訪問publicSelectedKeys。spa

Selector建立

一般咱們使用Selector的靜態工程方法open()來建立Selector對象:

public static Selector open() throws IOException {
    return SelectorProvider.provider().openSelector();
}

open方法負責向SPI發出請求,獲取一個SelectorProvider實例。SelectorProvider的靜態工廠方法 provider()決定由哪一個SelectorProvider對象來建立給定的Selector實例,一般是一個DefaultSelectorProvider實例。不一樣操做系統對應着不一樣的sun.nio.ch.DefaultSelectorProvider,Linux下DefaultSelectorProvider.create()會生成一個sun.nio.ch.EPollSelectorProvider類型的SelectorProvider,Windows環境下則生成sun.nio.ch.WindowsSelectorProvider類型的SelectorProvider。

當獲取到SelectorProvider實例後,調用它的openSelector()便可建立一個特定的Selector對象。

Selection操做

Selector中提供了3種類型的selection操做:

select():該方法會一直阻塞直到至少一個channel中有感興趣的事件發生,除非當前線程發生中斷或selector的wakeup方法被調用;

select(long timeout):該方法與select()相似,會一直阻塞直到至少一個channel中有感興趣的事件發生,除非下面3種狀況任意一種發生:1 設置的超時時間到達;2 當前線程發生中斷;3 selector的wakeup方法被調用;

selectNow():該方法不會發生阻塞,不管是否有channel發生就緒事件,都會當即返回。

選擇器中最重要的就是selection操做,當咱們調用Selector的select方法時,selectedKeys集合會被更新,經過遍歷selectedKeys,能夠找到已經就緒的通道,從而處理各類I/O事件。select操做的大概過程以下:

  1. 檢查cancelledKeys集合,若是它非空,從keys集合中移除全部存在於cancelledKeys集合中的SelectionKey對象,並將註銷其通道,同時清空cancelledKeys;
  2. 向內核發起一個系統調用進行查詢,以肯定選擇器上註冊的每一個通道所關心的事件是否就緒。若是沒有通道已經準備好,線程可能會一直阻塞、阻塞指定時間,或當即返回,這主要依賴於特定select方法的調用;
  3. 系統調用返回,再次檢查cancelledKeys集合;
  4. 系統調用返回後,對於那些沒有就緒事件的通道將不會有任何的操做,對於那些已經有就緒事件的通道,將執行如下兩種操做的一種: 
  • 若是通道的SelectionKey還未加入selectedKeys集合,將其添加到selectedKeys集合中,並修改ready集合,以便準確地標識該通道當前有哪些準備好的操做。先前記錄在ready集合中的任何就緒信息都會被拋棄;
  • 不然,通道的SelectionKey已經存在於selectedKeys集合,修改ready集合,以便準確地標識該通道當前有哪些準備好的操做。全部以前記錄在ready集合中已經再也不是就緒狀態的操做不會被清除。事實上,全部的比特位都不會被清理。由操做系統決定的ready集合是與以前的ready集合按位分離的,一旦鍵被放置於選擇器的已選擇的鍵的集合中,它的ready集合將是累積的。比特位只會被設置,不會被清理。

select操做返回的值是ready集合在步驟2中被修改的鍵的數量,而不是selectedKeys集合中的通道總數。返回值不是已準備好的通道的總數,而是從上一個select( )調用以後進入就緒狀態的通道的數量。以前的調用中就緒的,而且在本次調用中仍然就緒的通道不會被計入,而那些在前一次調用中已經就緒但已經再也不處於就緒狀態的通道也不會被計入。這些通道可能仍然在已選擇的鍵的集合中,但不會被計入返回值中。返回值多是0。

Selector喚醒

Selector中提供了使線程從被阻塞的select( )方法中優雅地退出的能力:

public abstract Selector wakeup();

若是一個線程在調用select()或select(long)方法時被阻塞,調用wakeup()會使線程當即從阻塞中喚醒;若是調用wakeup()期間沒有select操做,下次調用select相關操做會當即返回。在Select期間,屢次調用wakeup()與調用一次效果是同樣的。關於wakeup()的實現原理可參見文章Java NIO Selector的wakeup實現原理

Selector關閉

當咱們調用Selector的close()方法時,會首先執行wakeup操做,任何一個在選擇操做中阻塞的線程都將被喚醒。同時會註銷綁定在選擇器上的全部通道,釋放與此選擇器相關聯的任何其餘資源。

若是選擇已經處於關閉狀態,再次調用close()方法不會由任何做用。若調用該選擇器除close()和wakeup()以外的操做都會致使ClosedSelectorException異常。

SelectionKey

SelectionKey對象表明着一個Channel和它註冊的Selector間的關係。其channel( )方法可返回與該鍵相關的SelectableChannel對象,而selector( )則返回相關的Selector對象。此外,SelectionKey中包含兩個重要屬性,兩個以整數形式進行編碼的比特掩碼:

  • interestOps:表明對註冊Channel所感興趣的事件集合。interest集合是使用註冊通道時給定的值初始化的,能夠經過調用鍵對象的interestOps( int ops)方法修改。同時,能夠調用鍵對象的interestOps()方法獲取當前interest集合。當相關的Selector上的select( )操做正在進行時改變鍵的interest集合,不會影響那個正在進行的選擇操做。全部更改將會在select( )的下一個調用中體現出來;
  • readyOps:表明interest集合中從上次調用select( )以來已經就緒的事件集合,它是interestOps的子集。註冊通道時,初始化爲0,只有在選擇器選擇操做期間可能被更新。能夠調用鍵對象的readyOps()方法獲取當前ready集合。需注意的是ready集合返回的就緒狀態只是一個提示,不是保證。底層的通道在任什麼時候候都會不斷改變。其餘線程可能在通道上執行操做並影響它的就緒狀態。

SelectionKey中使用了四個常量來表明事件類型:

SelectionKey.OP_READ:通道已經準備好進行讀取;

SelectionKey.OP_WRITE:通道已經準備好寫入;

SelectionKey.OP_CONNECT:通道對應的socket已經準備好鏈接;

SelectionKey.OP_ACCEPT:通道對應的server socket已經準備好接受一個新鏈接。

註冊通道時,若是咱們不止對一種操做感興趣,能夠用「位或」操做符將多個常量鏈接起來。以下:

socketChannel.register(selector, SelectionKey.OP_CONNECT|SelectionKey.OP_READ|SelectionKey.OP_WRITE);

在一次selection以後,咱們可使用如下幾個方法來檢測channel中什麼事件已經就緒:

selectionKey.isAcceptable():是否已準備好接受新鏈接;
selectionKey.isConnectable():是否已準備好鏈接;
selectionKey.isReadable():是否已準備好讀取;
selectionKey.isWritable():是否已準備好寫入。

咱們也可使用相關的比特掩碼來檢測就緒狀態,與調用上面的方法是一致的。如:

if ((selectionkey.readyOps( ) & SelectionKey.OP_READ) != 0) {
    readBuffer.clear( ); 
    key.channel( ).read (readBuffer); 
    ... 
}

一個selectionKey被建立後將保持有效,調用selectionKey的cancel()方法或關閉其通道或關閉其選擇器將致使其失效。咱們能夠調用isValid( )方法來檢查selectionKey是否仍然有效。當咱們調用selectionKey的cancel()方法後,它將被放在相關的選擇器的cancelledKeys集合中。註冊關係不會當即被取消,可是selectionKey會當即失效。當再次調用select( )方法時(或者一個正在進行的select()調用結束時),cancelledKeys中的被取消的鍵將被清理掉。

selectionKey除了維護Channel和Selector的註冊關係外,還提供了保存「附件」的功能,並提供方法訪問它。這是一種容許咱們將任意對象與鍵關聯的便捷方法。這個對象能夠引用任何對象,例如業務對象、會話句柄、其餘通道等等。當咱們在遍歷與選擇器相關的鍵時,可使用附加在selectionKey上的對象句柄來獲取相關的上下文。attach( )方法將在selectionKey中保存所提供的對象的引用,attachment( )方法則用來獲取與selectionKey關聯的附件句柄。

關於SelectionKey還有最後一點須要注意,SelectionKey是線程安全的。修改interest集合的操做是經過Selector對象進行同步的,而選擇器所使用的鎖策略是依賴於具體實現的。所以若是Selector正在進行選擇操做,則讀取或寫入interest集可能會阻塞不肯定的時間。

示例代碼

selector=Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
    selector.select();
    Set<SelectionKey> selectionKeys=selector.selectedKeys();
    Iterator<SelectionKey> iterator=selectionKeys.iterator();
    while(iterator.hasNext()){
        SelectionKey selectionKey=iterator.next();
        if(selectionKey.isAcceptable()){
            // a connection was accepted by a ServerSocketChannel.
        }else if(selectionKey.isReadable()){
            // a channel is ready for reading
        }else if(selectionKey.isWritable()){
           // a channel is ready for writing
        }
        iterator.remove();
    }
}

完整的示例代碼可移步https://github.com/JeffreyHy/jeffery-nio-study/tree/master/nio-demo

歡迎指出本文有誤的地方,轉載請註明原文出處https://my.oschina.net/7001/blog/1556102

相關文章
相關標籤/搜索