Java 7源碼分析第20篇

來源:互聯網
上載者:User

public abstract void wakeup();  

Selector(選取器)是Java NIO中能夠檢測一到多個NIO通道,並判斷是否為讀寫事件做好準備。這樣,一個單獨的線程可以管理多個channel,從而管理多個網路連接。僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。事實上,可以只用一個線程處理所有的通道。對於作業系統來說,線程之間環境切換的開銷很大,而且每個線程都要佔用系統的一些資源(如記憶體)。因此,使用的線程越少越好。

在真正使用選取器時,涉及到的主要類有3個:

  • 選取器(Selector):選取器類管理著一個被註冊通道集合的資訊和它們的就緒狀態。通道和選取器一起被註冊的,並且使用選取器來更新通道的就緒狀態。      
  • 可選擇通道(SelectableChannel):它是所有支援就緒檢查的通道類的父類。FileChannel 對象不是可選擇的,因為它沒有繼承 SelectableChannel。所有 socket 通道都是可選擇的,包括從管道(Pipe)對象的中獲得的通道。只有 SelectableChannel 可以被註冊到 Selector 對象上。一個通道可以被註冊到多個選取器上,但對每個選取器而言只能被註冊一次。       
  • 選擇鍵(SelectionKey):選擇鍵封裝了特定的通道與特定的選取器的註冊關係。選擇鍵對象被 SelectableChannel.register() 返回並提供一個表示這種註冊關係的標記。選擇鍵包含了兩個位元集(以整數的形式進行編碼),指示了該註冊關係所關心的通道操作,以及通道已經準備好的操作。
先來看一下SelectableChannel類的原始碼,如下:

public abstract class SelectableChannel extends AbstractChannel implements Channel {    public abstract SelectionKey register(Selector sel, int ops)    throws ClosedChannelException;    public abstract SelectionKey register(Selector sel, int ops, Object att)    throws ClosedChannelException;    public abstract boolean isRegistered();    public abstract SelectionKey keyFor(Selector sel);    public abstract int validOps();    public abstract void configureBlocking(boolean block) throws IOException;    public abstract boolean isBlocking();    public abstract Object blockingLock();}

調用register() 方法會將一個非阻塞通道註冊到一個選取器上。如果試圖註冊一個處於阻塞狀態的通道,register() 將拋出未檢查的 IllegalBlockingModeException 異常。此外,通道一旦被註冊,就不能回到阻塞狀態。試圖這麼做的話,將在調用 configureBlocking() 方法時將拋出 IllegalBlockingModeException 異常。並且,理所當然地,試圖註冊一個已經關閉的 SelectableChannel 執行個體的話,也將拋出 ClosedChannelException 異常,就像方法原型指示的那樣。通道在被註冊到一個選取器上之前,必須先設定為非阻塞模式(通過調用configureBlocking(false))。如下:

channel.configureBlocking(false);// 設定為非阻塞模式SelectionKey key = channel.register(selector,Selectionkey.OP_READ);// 調用方法進行

與Selector一起使用時,Channel必須處於非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式。而通訊端通道都可以。

register()方法的第二個參數可以指定監聽四種不同類型的事件,這四種事件用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;
具體的解釋如下:

通道觸發了一個事件意思是該事件已經就緒。如果你對不止一種事件感興趣,那麼可以用“位或”操作符將常量串連起來,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

下面來看一下SelectionKey抽象類別中定義的方法,如下:

public abstract class SelectionKey {    public abstract SelectableChannel channel();// 擷取Channel對象    public abstract Selector selector();  // 擷取Selector對象    public abstract void cancel();    public abstract boolean isValid();}

 一個鍵表示了一個特定的通道對象和一個特定的選取器對象之間的註冊關係。前兩個方法中反映了這種關係:channel() 方法返回與該鍵相關的 SelectableChannel 對象,而 selector() 則返回相關的 Selector 對象。

 鍵對象表示了一種特定的註冊關係。當應該終結這種關係的時候,可以調用 SelectionKey 對象的 cancel() 方法。可以通過調用 isValid() 方法來檢查它是否仍然表示一種有效關係。當鍵被取消時,它將被放在相關的選取器的已取消的鍵的集合裡。註冊不會立即被取消,但鍵會立即失效。 

 當通道關閉時,所有相關的鍵會自動取消(記住,一個通道可以被註冊到多個選取器上)。當選取器關閉時,所有被註冊到該選取器的通道都將被登出,並且相關的鍵將立即被無效化(取消)。一旦鍵被無效化,調用它的與選擇相關的方法就將拋出 CancelledKeyException。

    public abstract int interestOps();    public abstract void interestOps(int ops);    public abstract int readyOps();   

一個 SelectionKey 對象包含兩個以整數形式進行編碼的位元掩碼:一個用於指示那些通道/選取器組合體所關心的操作(instrest 集合),另一個表示通道準備好要執行的操作(ready 集合)。 
 當前的 interest 集合可以通過調用鍵對象的 interestOps() 方法來擷取。最初,這應該是通道被註冊時傳進來的值。這個 interset 集合永遠不會被選取器改變,但你可以通過調用 interestOps() 方法並傳入一個新的位元掩碼參數來改變它。 
 interest 集合也可以通過將通道註冊到選取器上來改變(實際上使用一種迂迴的方式調用 interestOps())。當相關的 Selector 上的 select() 操作進行中時改變鍵的 interest 集合,不會影響那個進行中的選擇操作。所有更改將會在 select() 的下一個調用中體現出來。 interest集合是你所選擇的感興趣的事件集合。可以通過SelectionKey中的interestOps()方法讀寫interest集合,如下:

int interestSet = selectionKey.interestOps();boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

可以看到,用“位與”操作interest 集合和給定的SelectionKey常量,可以確定某個確定的事件是否在interest 集合中。

 可以通過調用鍵的 readyOps() 方法來擷取相關的通道的已經就緒的操作。ready 集合是 interest 集合的子集,並且表示了 interest 集合中從上次調用 select() 以來已經就緒的那些操作。 ready 集合是通道已經準備就緒的操作的集合。在一次選擇(Selection)之後,你會首先調用readyOps()方法訪問ready集合:

int readySet = selectionKey.readyOps();

 就像之前提到過的那樣,有四個通道操作可以被用於測試就緒狀態。你可以像上面的代碼那樣,通過測試位元掩碼來檢查這些狀態,但 SelectionKey 類定義了四個便於使用的布爾方法來為你測試這些位元值。每一個方法都與使用特定掩碼來測試 readyOps( )方法的結果的效果相同: 

 public final boolean isReadable(); public final boolean isWritable(); public final boolean isConnectable(); public final boolean isAcceptable();


if (key.isWritable())等價於:if ((key.readyOps() & SelectionKey.OP_WRITE) != 0)
這四個方法在任意一個 SelectionKey 對象上都能安全地調用。不能在一個通道上註冊一個它不支援的操作,這種操作也永遠不會出現在 ready 集合中。調用一個不支援的操作將總是返回 false,因為這種操作在該通道上永遠不會準備好。 
通過相關的選擇鍵的 readyOps() 方法返回的就緒狀態指示只是一個提示,不是保證。底層的通道在任何時候都會不斷改變。其他線程可能在通道上執行操作並影響它的就緒狀態。同時,作業系統的特點也總是需要考慮的。 


public final Object attach(Object ob);public final Object attachment();

      這兩  個方法允許你在鍵上放置一個“附件”,並在後面擷取它。這是一種允許你將任意對象與鍵關聯的便捷的方法。這個對象可以引用任何對你而言有意義的對象,例如業務對象、交談控制代碼、其他通道等等。這將允許你遍曆與選取器相關的鍵,使用附加在上面的物件控點作為引用來擷取相關的上下文。 

attach() 方法將在鍵對象中儲存所提供的對象的引用。SelectionKey 類除了儲存它之外,不會將它用於任何其他用途。任何一個之前儲存在鍵中的附件引用都會被替換。可以使用 null 值來清除附件。可以通過調用 attachment() 方法來擷取與鍵關聯的附件控制代碼。 

        如果選擇鍵的存續時間很長,但你附加的對象不應該存在那麼長時間,請記得在完成後清理附件。否則,你附加的對象將不能被記憶體回收,你將會面臨記憶體流失問題。 

        回憶下 SelectableChannel 的 register() 方法,有個接受一個 Object 參數的重載方法如下: 

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, myObject);等價於:SelectionKey key = channel.register(selector, SelectionKey.OP_READ);key.attach (myObject);

       關於 SelectionKey 的最後一件需要注意的事情是並發性。總體上說,SelectionKey 對象是安全執行緒的,但知道修改 interest 集合的操作是通過 Selector 對象進行同步的是很重要的。這可能會導致 interestOps() 方法的調用會阻塞不確定長的一段時間。選取器所使用的鎖策略(例如是否在整個選擇過程中保持這些鎖)是依賴於具體實現的。幸好,這種多元處理能力被特別地設計為可以使用單線程來管理多個通道。被多個線程使用的選取器也只會在系統特別複雜時產生問題。

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

public abstract class Selectory {    // This is a partial API listing    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();}

  • 登入的鍵的集合(Registered key set)        與選取器關聯的已經註冊的鍵的集合。並不是所有註冊過的鍵都仍然有效。這個集合通過 keys() 方法返回,並且可能是空的。這個登入的鍵的集合不是可以直接修改的;試圖這麼做的話將引 java.lang.UnsupportedOperationException。        
  • 已選擇的鍵的集合(Selected key set)        登入的鍵的集合的子集。這個集合的每個成員都是相關的通道被選取器(在前一個選擇操作中)判斷為已經準備好的,並且包含於鍵的 interest 集合中的操作。這個集合通過 selectedKeys() 方法返回(並有可能是空的)。        
           不要將已選擇的鍵的集合與 ready 集合弄混了。這是一個鍵的集合,每個鍵都關聯一個已經準備好至少一種操作的通道。每個鍵都有一個內嵌的 ready 集合,指示了所關聯的通道已經準備好的操作。    
            鍵可以直接從這個集合(已選擇的鍵的集合)中移除,但不能添加。試圖向已選擇的鍵的集合中添加元素將拋出 java.lang.UnsupportedOperationException。       
  • 已取消的鍵的集合(Cancelled key set)       登入的鍵的集合的子集,這個集合包含了 cancel() 方法(SelectionKey 類的方法)被調用過的鍵(這個鍵已經被無效化),但它們還沒有被登出 。這個集合是選取器對象的私人成員,因而無法直接存取。在一個剛初始化的 Selector 對象中,以上提到的三個集合都是空的。 
    ◇ 選擇過程 
        Selector 類的核心是選擇過程。基本上來說,選取器是對 select()、poll() 等本地調用(native call)或者類似的作業系統特定的系統調用的一個封裝。但是 Selector 所作的不僅僅是簡單地向本地代碼傳送參數。它對每個選擇操作應用了特定的過程。對這個過程的理解是合理地管理鍵和它們所表示的狀態資訊的基礎。 
        Selector 類的 select() 方法有以下三種不同的形式: 
  • int select()        這種調用在沒有通道就緒時將無限阻塞。一旦至少有一個登入的通道就緒,選取器的選擇鍵就會被更新,並且每個就緒的通道的 ready 集合也將被更新。返回值將會是已經確定就緒的通道的數目。正常情況下,這些方法將返回一個非零的值,因為直到一個通道就緒前它都會阻塞。        
  • int select(long timeout)        這種調用與之前的例子完全相同,除了如果在你提供的逾時時間(以毫秒計算)內沒有通道就緒時,它將返回 0。如果一個或者多個通道在時間限制終止前就緒,鍵的狀態將會被更新,並且方法會在那時立即返回。將逾時參數指定為 0 表示將無限期等待,那麼它就在各個方面都等同於使用無參數版本的 select() 了。        
  • int selectNow()        這種種形式是完全非阻塞的。selectNow() 方法執行就緒檢查過程,但不阻塞。如果當前沒有通道就緒,它將立即返回 0。 
            選擇操作是當三種形式的 select() 中的任意一種被調用時,由選取器執行的。不管是哪一種形式的調用,下面步驟將被執行: 
        1. 已取消的鍵的集合將會被檢查。如果它是非空的,每個已取消的鍵的集合中的鍵將從另外兩個集合中移除,並且相關的通道將被登出。這個步驟結束後,已取消的鍵的集合將是空的。 
        2. 登入的鍵的集合中的鍵的 interest 集合將被檢查。在這個步驟中的檢查執行過後,對 interest 集合的改動不會影響剩餘的檢查過程。 
        一旦就緒條件被定下來,底層作業系統將會進行查詢,以確定每個通道所關心的操作的真實就緒狀態。依賴於特定的 select() 方法調用,如果沒有通道已經準備好,線程可能會在這時阻塞,通常會有一個逾時值。對於那些還沒準備好的通道將不會執行任何的操作。對於那些作業系統指示至少已經準備好 interest 集合中的一種操作的通道,將執行以下兩種操作中的一種: 

  • 如果通道的鍵還沒有處於已選擇的鍵的集合中,那麼鍵的 ready 集合將被清空,然後表示作業系統發現的當前通道已經準備好的操作的位元掩碼將被設定。
  • 否則,也就是鍵在已選擇的鍵的集合中。鍵的 ready 集合將被表示作業系統發現的當前已經準備好的操作的位元掩碼更新。所有之前的已經不再是就緒狀態的操作不會被清除。事實上,所有的位元位都不會被清理。由作業系統決定的 ready 集合是與之前的 ready 集合按位分離的,一旦鍵被放置於選取器的已選擇的鍵的集合中,它的 ready 集合將是累積的。位元位只會被設定,不會被清理。假設之前的 ready 集合為 100,此次 010 的操作已就緒,此時的 ready 集合為 110,而不是 010。這就是累積,不會被清理。
        3. 步驟 2 可能會花費很長時間,特別是所激發的線程處於休眠狀態時。與該選取器相關的鍵可能會同時被取消。當步驟 2 結束時,步驟 1 將重新執行,以完成任意一個在選擇進行的過程中,鍵已經被取消的通道的登出。 
        4. select 操作返回的值是 ready 集合在步驟 2 中被修改的鍵的數量,而不是已選擇的鍵的集合中的通道的總數。返回值不是已準備好的通道的總數,而是從上一個 select() 調用之後進入就緒狀態的通道的數量。之前的調用中就緒的,並且在本次調用中仍然就緒的通道不會被計入,而那些在前一次調用中已經就緒但已經不再處於就緒狀態的通道也不會被計入。這些通道可能仍然在已選擇的鍵的集合中,但不會被計入返回值中。返回值可能是 0。 

    ◇ 停止選擇過程 
        Selector 的 API 中的最後一個方法,wakeup(),提供了使線程從被阻塞的 select() 方法中優雅地退出的能力: 
public abstract void wakeup();   
有三種方式可以喚醒在 select() 方法中睡眠的線程: 
  • 調用 wakeup()        調用 Selector 對象的 wakeup() 方法將使得選取器上的第一個還沒有返回的選擇操作立即返回。        
  • 調用 close()        如果選取器的 close() 方法被調用,那麼任何一個在選擇操作中阻塞的線程都將被喚醒,就像 wakeup() 方法被調用了一樣。與選取器相關的通道將被登出,而鍵將被取消。        
  • 調用 interrupt()        如果睡眠中的線程的 interrupt() 方法被調用,它的返回狀態將被設定。如果被喚醒的線程之後將試圖在通道上執行 I/O 操作,通道將立即關閉,然後線程將捕捉到一個異常。使用 wakeup() 方法將會優雅地將一個在 select() 方法中睡眠的線程喚醒。如果你想讓一個睡眠的線程在直接中斷之後繼續執行,需要執行一些步驟來清理中斷狀態(參見 Thread.interrupted() 的相關文檔)。
        Selector 對象將捕捉 InterruptedException 異常並調用 wakeup() 方法。請注意這些方法中的任意一個都不會關閉任何一個相關的通道。中斷一個選取器與中斷一個通道是不一樣的。選取器不會改變任意一個相關的通道,它只會檢查它們的狀態。當一個在 select() 方法中睡眠的線程中斷時,對於通道的狀態而言,是不會產生歧義的。 

    ◇ 管理選擇鍵 
        選擇是累積的。一旦一個選取器將一個鍵添加到它的已選擇的鍵的集合中,它就不會移除這個鍵。並且,一旦一個鍵處於已選擇的鍵的集合中,這個鍵的 ready 集合將只會被設定,而不會被清理。乍一看,這好像會引起麻煩,因為選擇操作可能無法表現出登入的通道的正確狀態。它提供了極大的靈活性,但把合理地管理鍵以確保它們表示的狀態資訊不會變得陳舊的任務交給了程式員。 

        合理地使用選取器的秘訣是理解選取器維護的選擇鍵集合所扮演的角色。最重要的部分是當鍵已經不再在已選擇的鍵的集合中時將會發生什麼。當通道上的至少一個感興趣的操作就緒時,鍵的 ready 集合就會被清空,並且當前已經就緒的操作將會被添加到 ready 集合中。該鍵之後將被添加到已選擇的鍵的集合中。 

        清理一個 SelectKey 的 ready 集合的方式是將這個鍵從已選擇的鍵的集合中移除。選擇鍵的就緒狀態只有在選取器對象在選擇操作過程中才會修改。處理思想是只有在已選擇的鍵的集合中的鍵才被認為是包含了合法的就緒資訊的。這些資訊將在鍵中長久地存在,直到鍵從已選擇的鍵的集合中移除,以通知選取器你已經看到並對它進行了處理。如果下一次通道的一些感興趣的操作發生時,鍵將被重新設定以反映當時通道的狀態並再次被添加到已選擇的鍵的集合中。 

        這種架構提供了很多靈活性。通常的做法是在選取器上調用一次 select 操作(這將更新已選擇的鍵的集合),然後遍曆 selectKeys() 方法返回的鍵的集合。在按順序進行檢查每個鍵的過程中,相關的通道也根據鍵的就緒集合進行處理。然後鍵將從已選擇的鍵的集合中被移除(通過在 Iterator 對象上調用 remove() 方法),然後檢查下一個鍵。完成後,通過再次調用 select() 方法重複這個迴圈。


























聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.