天天看點

JAVA NIO源碼分析---總結篇一、源碼分析流程梳理。二、相關知識點三、SelectionKey二、中斷select()的方法

通過上一篇對JAVA NIO的源碼分析,對一些重要的代碼實作進行了探究,現将從源碼分析中得出的結論總結如下。

一、源碼分析流程梳理。

1.Selector.open() 擷取選擇器的時候,根據不同的作業系統建立Selector實作類,實作類建立了用于儲存通道句柄和事件類型的資料結構PollArrayWrapper,如果是Windows系統将會建立一對互相連接配接的socket通道模拟管道用于喚醒,而Linux對于核心版本>=2.6并且JDK>1.5u9的情況下可以建立更加高效的epoll模式的Selector實作類。并且Linux系統可以直接利用作業系統的管道做到喚醒的功能。

2.ServerSocketChannel.register(...)通道注冊,根據channel和selector建立了把兩者關聯起來的SelectionKeyImpl對象,并将其記錄到已注冊鍵的集合中,同時将socket句柄以及事件添加至PollArrayWrapper結構中。另外針對作業系統對最大句柄數的限制可能還需要建立更多的線程。

3.Selector.select();在執行選擇操作之前,首先對已經取消的鍵集合進行清理,并調整線程數(因最大句柄數限制而建立的helper線程),然後調用作業系統底層的select函數,把檢查通道是否就緒的工作移交至作業系統,如果是Windows則是通過輪詢的方式進行檢測,如果是Linux則可能是select/poll也是輪詢或者基于中斷的更高效的epoll的方式進行檢測,這取決于jdk的版本和核心的版本。當系統底層的select調用傳回之後,再次進行對已經取消的鍵集合進行清理,并傳回繼上一次select操作之後到本次結束新就緒的通道數量。

4.Selector.wakeup(),通過向管道(Windows是互聯的socket通道)的sink端寫入一個位元組來喚醒阻塞在select的調用,并且連續多次的喚醒動作将等同于一次調用。

二、相關知識點

1.首先Selector.open()并不是單例模式,當你每次調用該靜态方法時候,都傳回一個全新的Selector執行個體。

2.Selector能夠通過調用configureBlocking來設定是否啟用非阻塞模式。其預設為阻塞模式。

3.服務端和用戶端是否維護着同一份Selector,答案是否定的,服務端和用戶端各自維護着一個Selector對象,并且注意在多線程并發的時候,雖然Selector是線程安全的,但是其内部的重要成員集合(registeredKeys、selectedKeys、cancelledKeys)是非線程安全的。如果在多個線程并發地通路一個選擇器的鍵的集合的時候存在任何問題,您可以采取一些步驟來合理地同步通路。

4.在執行選擇操作時,選擇器在 Selector 對象上進行同步,然後是已注冊的鍵的集合,最後是已選擇的鍵的集合,按照這樣的順序。 在多線程的場景中,如果您需要對任何一個鍵的集合進行更改,不管是直接更改還是其他操作帶來的副作用,您都需要首先以相同的順序,在同一對象上進行同步。鎖的過程是非常重要的。如果競争的線程沒有以相同的順序請求鎖,就将會有死鎖的潛在隐患。如果您可以確定否其他線程不會同時通路選擇器,那麼就不必要進行同步了。

5.channel.read()函數會傳回-1,那麼什麼時候會讀到-1呢?針對伺服器端而言,當用戶端調用了channel.close()關閉連接配接時,這時候伺服器端傳回的讀取數是-1,表示已經到了末尾。那麼此時需要把對應的SelectionKey給cancel掉,表示selector不再監聽這個channel上的讀事件,并且關閉channel。

6.雖然說一個通道可以被注冊到多個選擇器上,但對每個選擇器而言隻能被注冊一次。

7.當通道關閉時,所有相關的鍵會自動取消;當選擇器關閉時,所有被注冊到該選擇器的通道都将被登出,并且相關的鍵将立即被無效化。

8.注意

select()

操作傳回值不是已經準備好的通道的總數,而是從上一個

select()

調用之後進入就緒狀态的通道的數量。之前的調用中就緒的,并且在本次調用中仍然就緒的通道不會被計入,而那些在前一次調用中已經就緒但已經不再處于就緒狀态的通道也不會計入。這些通道可能仍然在已選擇的鍵的集合中,但不會被計入傳回值中,傳回值可能是0,這也是為何傳回0時,需要continue的原因。

9.Selector 類的 close( )方法與 select( )方法的同步方式是一樣的,是以也有一直阻塞的可能性。在選擇過程還在進行的過程中,所有對 close( )的調用都會被阻塞,直到選擇過程結束,或者執行選擇的線程進入睡眠。

10.當一個通道關閉時,它相關的鍵也就都被取消了。這并不會影響正在進行的select( ),但這意味着在您調用select( )之前仍然是有效的鍵,在傳回時可能會變為無效。

三、SelectionKey

selectionKey表示了通道(channel)與選擇器(selector)之間的注冊關系,以及維護了通道的事件。

SelectionKey 包含了兩個集合(其實是以整數形式進行編碼的byte掩碼):

1、 注冊的感興趣的操作集合 即:interestOps集合。

2、已經準備好的操作集合(就緒集合) 即:readyOps集合。

Selector維護了三個集合:

1、已經注冊的鍵集合 調用, keys() 

2、已經選擇的鍵集合 調用, selectedKeys() 

3、已經取消的鍵集合 私有, cancelledKeys

這些集合之間的流轉關系:

1. 當調用cancel操作的時候,隻是把要取消的鍵加入到了cancelledKeys鍵集合中,需要等到下次調用select的時候進行才會生效。但是SelectionKey的isValid()會立即回複false。

2. 在作業系統傳回就緒操作的通道的時候:

a)如果通道的selectionkey還沒有在已經選擇的鍵的集合(selectedKeys)中,那麼鍵的readyOps集合将被清空,然後表示作業系統發現的目前通道已經準備好的操作的比特掩碼将被設定。

b)否則,一旦通道的鍵被放入已經選擇的鍵的集合中時,ready集合不會被清除,而是累積。這就是說,如果之前的狀态是ready的操作,本次已經不是ready了,但是他的bit位依然表示是ready,不會被清除。 

從2可以看出我們為什麼每次循環selectedKeys的時候都需要調用it.remove().

另外:一個channel中的資料沒讀完(或有資料而不處理),那麼,這個channel一直處于就緒狀态中,是以每次selector的selectedKeys()方法總能傳回與這個channel關聯的Selectionkey,然後就會不停地循環select(),除非讀完channel中的資料,或者把這個SelectionKey給cancel掉。

二、中斷select()的方法

select()方法會阻塞住,等待有channel就緒才傳回。有時候,希望停止阻塞,中斷select方法,讓線程繼續。

有三種方法。 

1, wakeup()這是一種優雅的方法,立即傳回阻塞在select的線程。如果目前沒有阻塞在select上,則本次wakeup調用将作用在下一次select操作上,也就是下一次select調用将立即傳回無論是否有就緒發生。

2, close()選擇器的close被調用,則所有在選擇操作中阻塞的線程被喚醒,相關通道被登出,鍵也被取消。 

3, interrupt() 實際上interrupt并不會中斷線程。而是設定線程中斷标志。 

然後依然是調用wakeup()。這是因為 Selector 捕獲了interruptedException,然後在異常進行中調用了 wakeup()