Java NIO5:選擇器1---理論篇

五月的倉頡發表於2016-02-10

選擇器

最後,我們探索一下選擇器。由於選擇器內容比較多,所以本篇先偏理論地講一下,後一篇講程式碼,文章也沒有什麼概括、總結的,寫到哪兒算哪兒了,只求能將選擇器寫明白,並且將一些相對重要的內容加粗標紅。

選擇器提供選擇執行已經就緒的任務的能力,這使得多元I/O成為了可能,就緒執行和多元選擇使得單執行緒能夠有效地同時管理多個I/O通道。

某種程度上來說,理解選擇器比理解緩衝區和通道類更困難一些和複雜一些,因為涉及了三個主要的類,它們都會同時參與到這整個過程中,這裡先將選擇器的執行分解為幾條細節:

1、建立一個或者多個可選擇的通道(SelectableChannel)

2、將這些建立的通道註冊到選擇器物件中

3、選擇鍵會記住開發者關心的通道,它們也會追蹤對應的通道是否已經就緒

4、開發者呼叫一個選擇器物件的select()方法,當方法從阻塞狀態返回時,選擇鍵會被更新

5、獲取選擇鍵的集合,找到當時已經就緒的通道,通過遍歷這些鍵,開發者可以選擇對已就緒的通道要做的操作

對於選擇器的操作,大致就是這麼幾步,OK,接下去再進一步,看一下和選擇器相關的三個類。

 

選擇器、可選擇通道和選擇鍵類

選擇器(Selector)

選擇器類管理著一個被註冊的通道集合的資訊和它們的就緒狀態。通道是和選擇器一起被註冊的,並且使用選擇器來更新通道的就緒狀態。

可選擇通道(SelectableChannel)

這個抽象類提供了實現通道的可選擇性所需要的公共方法,它是所有支援就緒檢查的通道類的父類,FileChannel物件不是可選擇的,因為它們沒有繼承SelectableChannel,所有Socket通道都是可選擇的,包括從管道(Pipe)物件中獲得的通道。SelectableChannel可以被註冊到Selector物件上,同時可以設定對哪個選擇器而言哪種操作是感興趣的一個通道可以被註冊到多個選擇器上,但對每個選擇器而言只能被註冊一次

選擇鍵(SelectionKey)

選擇鍵封裝了特定的通道與特定的選擇器的註冊關係。呼叫SelectableChannel.register()方法會返回選擇鍵並提供一個表示這種註冊關係的標記。選擇鍵包含了兩個位元集(以整數形式進行編碼),指示了該註冊關係所關心的通道操作,以及通道已經準備好的操作。

用一張UML圖來描述一下選擇器、可選擇通道和選擇鍵:

 

建立選擇器

前面講了,選擇器的作用是管理了被註冊的通道集合和它們的就緒狀態,假設我們有三個Socket通道的選擇器,可能會有類似的程式碼:

...
Selector selector = Selector.open(); 
channel1.register(selector, SelectionKey.OP_READ); 
channel2.register(selector, SelectionKey.OP_WRITE);
channel3.register(selector, SelectionKey.OP_READ
| OP_WRITE);
channel4.register(selector, SelectionKey.OP_READ | OP_ACCEPT);
ready = selector.select(10000);
...

這種操作用圖表示就是:

程式碼建立了一個新的選擇器,然後將這四個(已經存在的)Socket通道註冊到選擇器上,而且感興趣的操作各不相同。select()方法在將執行緒置於睡眠狀態直到這些感興趣的事件中的一個發生或者10秒鐘過去,這就是所謂的事件驅動

再稍微看一下Selector的API細節:

public abstract class Selector
{
    ...
    public static Selector open() throws IOException;
    public abstract boolean isOpen();
    public abstract void close() throws IOException;
    public abstract SelectionProvider provider();
    ...
}

Selector是通過呼叫靜態工廠方法open()來例項化的,這個從前面的程式碼裡面也看到了,選擇器不是像通道或流那樣的基本I/O物件----資料從來沒有通過他們進行傳遞

通道是呼叫register方法註冊到選擇器上的,從程式碼裡面可以看到register()方法接受一個Selector物件作為引數,以及一個名為ops的整數型引數,第二個參數列示關心的通道操作。在JDK1.4中,有四種被定義的可選擇操作:讀(read)、寫(write)、連線(connect)和接受(accept)。

注意並非所有的操作都在所有的可選擇通道上被支援,例如SocketChannel就不支援accept。

 

使用選擇鍵

接下來看看選擇鍵,選擇鍵的API大致如下:

public abstract class SelectionKey
{
    public static final int OP_READ;
    public static final int OP_WRITE;
    public static final int OP_CONNECT;
    public static final int OP_ACCEPT;
    public abstract SelectableChannel channel();
    public abstract Selector selector();
    public abstract void cancel();
    public abstract boolean isValid();
    public abstract int interestOps();
    public abstract void iterestOps(int ops);
    public abstract int readyOps();
    public final boolean isReadable();
    public final boolean isWritable();
    public final boolean isConnectable();
    public final boolean isAcceptable();
    public final Object attach(Object ob);
    public final Object attachment();
}

關於這些API,總結幾點:

1、就像前面提到的,一個鍵表示了一個特定的通道物件和一個特定的選擇器物件之間的註冊關係,channel()方法和selector()方法反映了這種關係

2、開發者可以使用cancel()方法終結這種關係,可以使用isValid()方法來檢查這種有效的關係是否仍然存在,可以使用readyOps()方法來獲取相關的通道已經就緒的操作

3、第2點有提到readyOps()方法,不過我們往往不需要使用這個方法,SelectionKey類定義了四個便於使用的布林方法來為開發者測試通道的就緒狀態,例如:

if (key.isWritable()){...}

這種寫法就等價於:

if ((key.readyOps() & SelectionKeys.OPWRITE) != 0){...}

isWritable()、isReadable()、isConnectable()、isAcceptable()四個方法在任意一個SelectionKey物件上都能安全地呼叫。

4、當通道關閉時,所有相關的鍵會自動取消(一個通道可以被註冊到多個選擇器上);當選擇器關閉時,所有被註冊到該選擇器的通道都會被登出並且相關的鍵立即被取消

 

Selector維護的三種鍵

選擇器維護者註冊過的通道的集合,並且這些註冊關係中的任意一個都是封裝在SelectionKey物件中的。每一個Selector物件維護三種鍵的集合:

public abstract class Selector
{
    ...
    public abstract Set keys();
    public abstract Set selectedKeys();
    public abstract int select() throws IOException;
    public abstract int select(long timeout) throws IOException;
    public abstract int selectNow() throws IOException;
    public abstract void wakeup();
    ...   
}

由這個API看下去,這三種鍵是:

已註冊的鍵的集合(Registered key set)

與選擇器關聯的已經註冊的鍵的集合,並不是所有註冊過的鍵都有效,這個集合通過keys()方法返回,並且可能是空的。這些鍵的集合是不可以直接修改的,試圖這麼做將引發java.lang.UnsupportedOperationException。

已選擇的鍵的集合(Selected key set)

已註冊的鍵的集合的子集,這個集合的每個成員都是相關的通道被選擇器判斷為已經準備好的並且包含於鍵的interest集合中的操作。這個集合通過selectedKeys()方法返回(有可能是空的)。

鍵可以直接從這個集合中移除,但不能新增。試圖向已選擇的鍵的集合中新增元素將丟擲java.lang.UnsupportedOperationException。

已取消的鍵的集合(Cancelled key set)

已註冊的鍵的集合的子集,這個集合包含了cancel()方法被呼叫過的鍵(這個鍵已經被無效化),但它們還沒有被登出。這個集合是選擇器物件的私有成員,因而無法直接訪問。

 

選擇過程

接著就是Selector的核心選擇過程了。基本上來說,選擇器是對select()、poll()、epoll()等本地呼叫或者類似的作業系統特定的系統呼叫的一個包裝。但是Selector所做的不僅僅是簡單地向原生程式碼傳送引數,每個操作都有特定的過程,對這個過程的理解是合理地管理鍵和它們所表示的狀態資訊的基礎。

選擇操作是當三種形式的select()中的任意一種被呼叫時,由選擇器執行的。不管是哪一種形式的呼叫,下面步驟將被執行:

1、已取消的鍵的集合將會被檢查。如果它是非空的,每個已取消的鍵的集合中的鍵將從另外兩個集合中移除,並且相關的通道將被登出。此步驟結束,已取消的鍵的集合將是空的。

2、已註冊的鍵的集合中的鍵的interest集合將被檢查,此步驟結束,對interest集合的改動不會影響剩餘的檢查過程。一旦就緒條件被定下來,底層作業系統將會進行查詢,以確定每個通道所關心的操作的真實就緒狀態,依賴於特定的select()方法呼叫,如果沒有通道已經準備好,執行緒可能會在這時阻塞,通常會有一個超時值。

3、步驟2可能會花費很長時間,特別是執行緒處於阻塞狀態時。與該選擇器相關的鍵可能會同時被取消,當步驟2結束時,步驟1將重新執行,以完成任意一個在選擇進行的過程中,鍵已經被取消的通道的註冊。

4、select操作的返回值不是已準備好的通道的總數,而是從上一個select()呼叫之後進入就緒狀態的通道的數量。之前的呼叫中就緒的,並且在本次呼叫中仍然就緒的通道不會被計入,而那些在前一次呼叫中已經就緒但已經不再處於就緒狀態的通道也不會被計入。

最後,上面的Selector中還有兩個方法沒有提到,這裡說明一下它們的意思:

1、selectNow()

呼叫selectNow()方法執行就緒檢查過程,但不阻塞,如果當前沒有通道就緒,立刻返回0.

2、wakeup()

呼叫wakeup()方法將使得選擇器上的第一個還沒有返回的選擇操作立即返回,如果當前沒有正在進行中的選擇,那麼下一次對select()方法的一種形式的呼叫將立即返回,後續的選擇操作將正常進行。

相關文章