單一的線程使用就緒選擇來同時監控大量的通道
處于就緒狀態的通道就會等待Selector選擇
選擇器提供了詢問通道是否已經準備好執行每個IO操作的能力
就緒選擇的真正價值在于潛在的大量通道可以同時進行就緒狀態的檢查,Selector可以輕松決定選擇哪個通道
真正的就緒必須由操作系統來檢查,操作系統處理IO請求并通知各個線程它們的數據已經已經準備好了,而選擇器封裝了這種抽象
原因:java代碼來進行就緒選擇,甚至jvm來進行就緒檢查代價都十分昂貴,并且不夠原子性,甚至會破壞線程的正常進行
實際上只有三個類用于執行就緒選擇:
Selector:管理著一個被注冊的通道集合信息和他們的就緒狀態
SelectableChannel:被注冊到選擇器中,并且可以聲明對哪種操作感興趣
一個通道可以注冊到多個選擇器上
一個通道在一個選擇器上只能注冊一個操作集,該操作集不會疊加
SelectionKey:封裝了特定通道與特定選擇器的注冊關系,被SelectableChannel.register()返回
將SelectableChannel注冊到Selector上之后,Selector控制了通道的選擇過程
通道有四種操作:
SelectionKey.OP_ACCEPT,
SelectionKey.OP_CONNECT,
SelectionKey.OP_READ,
SelectionKey.OP_WRITE
SocketChannel不支持accept操作
處于就緒狀態的通道就會等待Selector選擇
選擇器提供了詢問通道是否已經準備好執行每個IO操作的能力
就緒選擇的真正價值在于潛在的大量通道可以同時進行就緒狀態的檢查,Selector可以輕松決定選擇哪個通道
真正的就緒必須由操作系統來檢查,操作系統處理IO請求并通知各個線程它們的數據已經已經準備好了,而選擇器封裝了這種抽象
原因:java代碼來進行就緒選擇,甚至jvm來進行就緒檢查代價都十分昂貴,并且不夠原子性,甚至會破壞線程的正常進行
實際上只有三個類用于執行就緒選擇:
Selector:管理著一個被注冊的通道集合信息和他們的就緒狀態
SelectableChannel:被注冊到選擇器中,并且可以聲明對哪種操作感興趣
一個通道可以注冊到多個選擇器上
一個通道在一個選擇器上只能注冊一個操作集,該操作集不會疊加
SelectionKey:封裝了特定通道與特定選擇器的注冊關系,被SelectableChannel.register()返回
SelectableChannel在注冊到選擇器上之前,先設置為非阻塞模式。
如果注冊一個阻塞模式的SelectableChannel將會拋出IllegalBlockingModeException異常
另外,已經注冊的SelectableChannel不能再修改阻塞模式,否則也會拋出IllegalBlockingModeException異常
注冊一個已經關閉的SelectableChannel,將會拋出ClosedChannelException異常如果注冊一個阻塞模式的SelectableChannel將會拋出IllegalBlockingModeException異常
另外,已經注冊的SelectableChannel不能再修改阻塞模式,否則也會拋出IllegalBlockingModeException異常
將SelectableChannel注冊到Selector上之后,Selector控制了通道的選擇過程
通道有四種操作:
SelectionKey.OP_ACCEPT,
SelectionKey.OP_CONNECT,
SelectionKey.OP_READ,
SelectionKey.OP_WRITE
SocketChannel不支持accept操作
SelectionKey.cancel()方法可以終結通道和選擇器的關系,然后SelectionKey會放在Selector.cancelledKeys() 的集合里
取消后SelectionKey會立即失效,但注冊不會立即取消,select()調用結束后,cancelledKeys將會處理,相應的注銷完成。
關閉通道時,所有的相關的SelectionKey都會自動取消
取消后SelectionKey會立即失效,但注冊不會立即取消,select()調用結束后,cancelledKeys將會處理,相應的注銷完成。
關閉通道時,所有的相關的SelectionKey都會自動取消
Selector關閉時,所有注冊的通道將會注銷,所有相關的SelectionKey會立即失效
一個SelectionKey實例中有兩個byte掩碼:
instrest集合:指示通道/選擇器結合體關心的操作
ready集合:通道已經就緒的操作
一個SelectionKey實例中有兩個byte掩碼:
instrest集合:指示通道/選擇器結合體關心的操作
ready集合:通道已經就緒的操作
instrest集合通過SelectionKey.interestOps()獲取,通過SelectionKey.interestOps(int ops)來改變
當相關的Selector正在select()操作時改變SelectionKey的instrest集合,不影響正在進行的select()操作,只會在下次select()時體現
當相關的Selector正在select()操作時改變SelectionKey的instrest集合,不影響正在進行的select()操作,只會在下次select()時體現
ready集合通過SelectionKey.readyOps()獲取,是instrest集合的子集,是上次select()操作后就緒的操作
最簡單的狀態測試方法是:
最簡單的狀態測試方法是:
SelectionKey.isAcceptable() 等價于 ((key.readyOps( ) & SelectionKey.OP_ACCEPT) != 0
SelectionKey.isConnectable() 等價于 ((key.readyOps( ) & SelectionKey.OP_CONNECT) != 0
SelectionKey.isReadable() 等價于 ((key.readyOps( ) & SelectionKey.OP_READ) != 0
SelectionKey.isWritable() 等價于 ((key.readyOps( ) & SelectionKey.OP_WRITE) != 0
SelectionKey.readyOps()返回的ready集合指示個提示,不是絕對的保證,因為底層的通道在任何時候都可能會被改變
SelectionKey是線程安全的,但修改instrest集合的操作是通過Selector進行同步的
這導致SelectionKey.interestOps()的調用會阻塞不確定長的時間
這導致SelectionKey.interestOps()的調用會阻塞不確定長的時間
Selector使用的鎖策略是依賴具體實現的
在單線程管理多個通道時不會出現問題,而多個線程共享Selector時會遇到同步的問題,到那時就應該重新設計
每個Selector都要同時維護三個SelectionKey集合:
已注冊的鍵的集合Registered key set,不能直接修改,只能通過SelectionKey.interestOps()重置
已選擇的鍵的集合Selected key set,其中每個SelectionKey都有一個內嵌的ready集合,可以直接移除其中的SelectionKey,但不能添加
已取消的鍵的集合Cancelled key set,是Selector的私有成員,無法直接訪問
在單線程管理多個通道時不會出現問題,而多個線程共享Selector時會遇到同步的問題,到那時就應該重新設計
每個Selector都要同時維護三個SelectionKey集合:
已注冊的鍵的集合Registered key set,不能直接修改,只能通過SelectionKey.interestOps()重置
已選擇的鍵的集合Selected key set,其中每個SelectionKey都有一個內嵌的ready集合,可以直接移除其中的SelectionKey,但不能添加
已取消的鍵的集合Cancelled key set,是Selector的私有成員,無法直接訪問
Selector的核心是select(),是對操作系統的本地調用(native call)的封裝,但是它不僅僅是向操作系統代碼傳遞參數,還對每個select操作應用不同的過程
select()的執行步驟:
1 檢查Cancelled key set。如果非空,每個取消的key將從另外兩個集合中移除,相關通道被注銷,執行完畢,Cancelled key set為空
2 檢查Registered key set中每個鍵的instrest集合,檢查中如果對instrest集合有改動,不會影響剩余的檢查過程
底層操作系統將會進行查詢,確定每個通道關心的操作的真實就緒狀態,依賴特定的select調用。如果沒有就緒的通道,則阻塞,直至超時
這個過程會使調用線程睡眠一段時間,直至通道的就緒狀態被確定下來
對于那些操作系統指示至少已經準備好instrest集合中的一種操作的通道,將執行下面的一種操作:
a 如果通道的SelectionKey沒有處于Selected key set中,那么SelectionKey的ready集合將會清空,表示當前通道已經準備好的操作的Byte掩碼將被設置
b 通道的SelectionKey已經處于Selected key set中,每個SelectionKey的ready集合更新為已經準備好的操作的Byte掩碼(SelectionKey.OP_READ ,SelectionKey.OP_WRITE等)
3 步驟2會花費很長時間,特別是所激發的線程處于休眠狀態時。步驟2結束時,步驟1會重新執行,以完成任意一個在選擇進行的過程當中,鍵已經被取消的通道的注銷
4 select()返回的值是步驟2中被修改的鍵的數量,即上一個select()調用之后進入就緒狀態的通道的數量,而不是Selected key set中通道的數量。之前的調用中已經就緒的,并且在本次調用中仍然就緒的通道不會被計入。返回值有可能為0
使用Cancelled key set來延遲注銷,是一種防止線程在取消鍵時阻塞,并防止與正在進行的選擇操作沖突的好方法
注銷通道是一種代價很高的操作,清理已取消的鍵,并在選擇操作之前或之后立即注銷通道,可以消除它們可能正好在選擇的過程中執行的潛在的問題。
1 檢查Cancelled key set。如果非空,每個取消的key將從另外兩個集合中移除,相關通道被注銷,執行完畢,Cancelled key set為空
2 檢查Registered key set中每個鍵的instrest集合,檢查中如果對instrest集合有改動,不會影響剩余的檢查過程
底層操作系統將會進行查詢,確定每個通道關心的操作的真實就緒狀態,依賴特定的select調用。如果沒有就緒的通道,則阻塞,直至超時
這個過程會使調用線程睡眠一段時間,直至通道的就緒狀態被確定下來
對于那些操作系統指示至少已經準備好instrest集合中的一種操作的通道,將執行下面的一種操作:
a 如果通道的SelectionKey沒有處于Selected key set中,那么SelectionKey的ready集合將會清空,表示當前通道已經準備好的操作的Byte掩碼將被設置
b 通道的SelectionKey已經處于Selected key set中,每個SelectionKey的ready集合更新為已經準備好的操作的Byte掩碼(SelectionKey.OP_READ ,SelectionKey.OP_WRITE等)
3 步驟2會花費很長時間,特別是所激發的線程處于休眠狀態時。步驟2結束時,步驟1會重新執行,以完成任意一個在選擇進行的過程當中,鍵已經被取消的通道的注銷
4 select()返回的值是步驟2中被修改的鍵的數量,即上一個select()調用之后進入就緒狀態的通道的數量,而不是Selected key set中通道的數量。之前的調用中已經就緒的,并且在本次調用中仍然就緒的通道不會被計入。返回值有可能為0
使用Cancelled key set來延遲注銷,是一種防止線程在取消鍵時阻塞,并防止與正在進行的選擇操作沖突的好方法
注銷通道是一種代價很高的操作,清理已取消的鍵,并在選擇操作之前或之后立即注銷通道,可以消除它們可能正好在選擇的過程中執行的潛在的問題。
Selector的select()有三種形式:
1 Selector.select()
該調用在沒有通道就緒時將無限阻塞,一旦有至少一個已注冊的通道就緒,Selector的選擇鍵將會立即更新,每個選擇鍵的ready集合也會被更新
2 Selector.select(long timeout)
在提供的超時時間內沒有就緒通道,將會返回0。如果設置timeout=0,則等同于select()
3 Selector.selectNow() 是完全非阻塞的,會立即返回值
該調用在沒有通道就緒時將無限阻塞,一旦有至少一個已注冊的通道就緒,Selector的選擇鍵將會立即更新,每個選擇鍵的ready集合也會被更新
2 Selector.select(long timeout)
在提供的超時時間內沒有就緒通道,將會返回0。如果設置timeout=0,則等同于select()
3 Selector.selectNow() 是完全非阻塞的,會立即返回值
Selector.wakeup() 提供了使線程從被阻塞的select()方法中優雅的退出的能力
同樣有三種方式喚醒select()方法中睡眠的線程:
1 Selector.wakeup() 將使Selector上第一個沒有返回的select()操作立即返回,如果當前沒有正在進行的select()操作,則下次select()操作會立即返回。
2 Selector.close() 被調用時,所有在select()操作中阻塞的線程都將被喚醒,Selector相關通道被注銷,相關SelectionKey將被取消
3 Thread.interrupt()被調用時,返回狀態將被設置。如果喚醒之后的線程試圖在通道上執行IO操作,通道會立即關閉,線程捕捉到一個異常
如果想讓一個睡眠線程在中斷之后繼續進行,需要執行一些步驟來清理中斷狀態
這些操作不會改變單個相關通道,中斷一個Selector和中斷一個通道是不一樣的。Selector只會檢查通道的狀態。
當一個select() 操作時睡眠的線程發生了中斷,對于通道狀態而言,是沒有歧義的
選擇器把合理的管理SelectionKey,以確保它們表示的狀態不會變得陳舊的任務交給了程序員
當SelectionKey已經不在選擇器的Selected key set中時,會發生什么
當通道上至少一個感興趣的操作就緒時,SelectionKey的ready集合會被清空,并且當前已經就緒的操作會被添加到ready集合里,該SelectionKey隨后會被添加到Selected key set中
清理一個SelectionKey的ready集合的方式是將這個key從Selected key set中移除
1 創建ServerSocketChannel,注冊到Selector中,感興趣的操作為accept
2 輪詢Selector的select()操作,從就緒key集合中遍歷key的ready集合,有accept則調用ServerSocketChannel的accept()方法獲取SocketChannel,其中包含接收到的socket的句柄。再將SocketChannel注冊到Selector中感興趣的操作為read
3 當下次select()操作中key的ready集合中有read時,開始做事
如果在多個線程并發的訪問一個Selector的key set時,需要合理地同步訪問。在select()操作時,先在Selector上進行同步,再是Registered key set,最后是Selected key set。按照這樣的順序,Cancelled key set就會在第一步和第三步之間保持同步。
在多線程的場景中,如果需要對任何一個key set進行更改,不管是直接更改還是其他操作帶來的副作用,都需要以相同的順序,在同一對象上進行同步。如果競爭的線程沒有以同樣的順序請求鎖,則會有死鎖的隱患。
一個cpu的時候使用一個線程來管理所有通道,是一個合適的解決方案,但會浪費其他cpu的運行能力
在大量通道上執行select()操作不會有太大開銷,因為大多數工作都是操縱系統完成的
方案1 管理多個Selector,并隨機分配通道不是一個好方案
方案2 所有通道使用一個Selector,將就緒通道的服務委托給其他線程。相當于用一個線程監控通道的就緒狀態,使用另外的工作線程池來處理接收到的數據。而線程池是可以調整的,或者動態調整。
在方案2中,如果某些通道要求更高的響應速度,可以用兩個選擇器來解決。并且線程池可以細化為日志線程池、命令線程池、狀態請求線程池等
同樣有三種方式喚醒select()方法中睡眠的線程:
1 Selector.wakeup() 將使Selector上第一個沒有返回的select()操作立即返回,如果當前沒有正在進行的select()操作,則下次select()操作會立即返回。
2 Selector.close() 被調用時,所有在select()操作中阻塞的線程都將被喚醒,Selector相關通道被注銷,相關SelectionKey將被取消
3 Thread.interrupt()被調用時,返回狀態將被設置。如果喚醒之后的線程試圖在通道上執行IO操作,通道會立即關閉,線程捕捉到一個異常
如果想讓一個睡眠線程在中斷之后繼續進行,需要執行一些步驟來清理中斷狀態
這些操作不會改變單個相關通道,中斷一個Selector和中斷一個通道是不一樣的。Selector只會檢查通道的狀態。
當一個select() 操作時睡眠的線程發生了中斷,對于通道狀態而言,是沒有歧義的
選擇器把合理的管理SelectionKey,以確保它們表示的狀態不會變得陳舊的任務交給了程序員
當SelectionKey已經不在選擇器的Selected key set中時,會發生什么
當通道上至少一個感興趣的操作就緒時,SelectionKey的ready集合會被清空,并且當前已經就緒的操作會被添加到ready集合里,該SelectionKey隨后會被添加到Selected key set中
清理一個SelectionKey的ready集合的方式是將這個key從Selected key set中移除
SelectionKey的就緒狀態只有在Selector的select() 操作過程中才會修改,因為只有Selected key set中的SelectionKey才被認為是包含了合法的就緒信息的,這些信息在SelectionKey中長久存在,知道key從Selected key set中移除,以通知Selector你已經看到并對它進行了處理。當下一次通道的感興趣的操作發生時,key將被重新設置以反映當時通道的狀態,并再次被添加到Selected key set中
這種框架提供了非常大的靈活性。
常規做法是先調用select() 操作,再遍歷selectKeys()返回的key的集合。在按順序進行檢查每個key的過程中,相關的通道也根據key的就緒集合進行處理。然后key從Selected key set中移除,檢查下一個key。完成后通過調用select() 重復循環。
服務器端使用方法這種框架提供了非常大的靈活性。
常規做法是先調用select() 操作,再遍歷selectKeys()返回的key的集合。在按順序進行檢查每個key的過程中,相關的通道也根據key的就緒集合進行處理。然后key從Selected key set中移除,檢查下一個key。完成后通過調用select() 重復循環。
1 創建ServerSocketChannel,注冊到Selector中,感興趣的操作為accept
2 輪詢Selector的select()操作,從就緒key集合中遍歷key的ready集合,有accept則調用ServerSocketChannel的accept()方法獲取SocketChannel,其中包含接收到的socket的句柄。再將SocketChannel注冊到Selector中感興趣的操作為read
3 當下次select()操作中key的ready集合中有read時,開始做事
Selector是線程安全的,但key set不是。Selector.keys 和 Selector.selectkeys() 返回的是Selector內部私有key set的引用。這個集合可能隨時被改變。迭代器Iterator是快速失敗的(fail-fast),如果迭代的時候key set發生改變,拋出ConcurrentModificationException。所有如果希望在多個線程間共享Selector或SelectionKey,則要對此做好準備。當你修改一個key的時候,可能會破壞另一個線程的Iterator
如果在多個線程并發的訪問一個Selector的key set時,需要合理地同步訪問。在select()操作時,先在Selector上進行同步,再是Registered key set,最后是Selected key set。按照這樣的順序,Cancelled key set就會在第一步和第三步之間保持同步。
在多線程的場景中,如果需要對任何一個key set進行更改,不管是直接更改還是其他操作帶來的副作用,都需要以相同的順序,在同一對象上進行同步。如果競爭的線程沒有以同樣的順序請求鎖,則會有死鎖的隱患。
Selector的close()和select()操作都會有一直阻塞的可能,當在select()的過程當中,所有對close()的調用都會被阻塞,直到select()結束,或者執行select()的線程進入睡眠。當select()的線程進入睡眠時,close()的線程獲得鎖后會立即喚醒select()的線程,并關閉Selector
如果不采取相同的順序同步,則key set中key的信息不保證是有效的,相關通道也不保證是打開的一個cpu的時候使用一個線程來管理所有通道,是一個合適的解決方案,但會浪費其他cpu的運行能力
在大量通道上執行select()操作不會有太大開銷,因為大多數工作都是操縱系統完成的
方案1 管理多個Selector,并隨機分配通道不是一個好方案
方案2 所有通道使用一個Selector,將就緒通道的服務委托給其他線程。相當于用一個線程監控通道的就緒狀態,使用另外的工作線程池來處理接收到的數據。而線程池是可以調整的,或者動態調整。
在方案2中,如果某些通道要求更高的響應速度,可以用兩個選擇器來解決。并且線程池可以細化為日志線程池、命令線程池、狀態請求線程池等