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

選擇器

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

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

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

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

2、將這些創建的通道註冊到選擇器對象中

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

4、開發者調用一個選擇器對象的select()方法,當方法從阻塞狀態返回時,選擇鍵會被更新

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

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

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

選擇器(Selector)

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

可選擇通道(SelectableChannel)

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

選擇鍵(SelectionKey)

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

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

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

建立選擇器

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

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

這種操作用圖表示就是:

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

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

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

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

Selector是通過調用靜態工廠方法open()來實例化的,這個從前面的代碼裡面也看到了,選擇器不是像通道或流那樣的基本I/O對象----數據從來沒有通過他們進行傳遞

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

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

使用選擇鍵

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

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

關於這些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對象維護三種鍵的集合:

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

由這個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()方法的一種形式的調用將立即返回,後續的選擇操作將正常進行。


分享到:


相關文章: