Java Nio注意事項
Selector :
public abstract class Selectorextends Object
可通過呼叫此類的 open
方法建立選擇器,該方法將使用系統的預設選擇器提供者建立新的選擇器。也可通過呼叫自定義選擇器提供者的
openSelector
方法來建立選擇器。通過選擇器的
close
方法關閉選擇器之前,它一直保持開啟狀態。
通過
SelectionKey
物件來表示可選擇通道到選擇器的註冊。選擇器維護了三種選擇鍵集:
-
鍵集 包含的鍵表示當前通道到此選擇器的註冊。此集合由
keys
方法返回。 -
已選擇鍵集 是這樣一種鍵的集合,即在前一次選擇操作期間,檢測每個鍵的通道是否已經至少為該鍵的相關操作集所標識的一個操作準備就緒。此集合由
selectedKeys
方法返回。已選擇鍵集始終是鍵集的一個子集。 -
已取消鍵集 是已被取消但其通道尚未登出的鍵的集合。不可直接訪問此集合。已取消鍵集始終是鍵集的一個子集。
在新建立的選擇器中,這三個集合都是空集合。
通過某個通道的
register
方法註冊該通道時,所帶來的副作用是向選擇器的鍵集中添加了一個鍵。在選擇操作期間從鍵集中移除已取消的鍵。鍵集本身是不可直接修改的。
不管是通過關閉某個鍵的通道還是呼叫該鍵的
cancel
方法來取消鍵,該鍵都被新增到其選擇器的已取消鍵集中。取消某個鍵會導致在下一次選擇操作期間登出該鍵的通道,而在登出時將從所有選擇器的鍵集中移除該鍵。
通過選擇操作將鍵新增到已選擇鍵集中。可通過呼叫已選擇鍵集的
remove
方法,或者通過呼叫從該鍵集獲得的
iterator 的
remove
方法直接移除某個鍵。通過任何其他方式從不會將鍵從已選擇鍵集中移除;特別是,它們不會因為影響選擇操作而被移除。不能將鍵直接新增到已選擇鍵集中。
選擇
在每次選擇操作期間,都可以將鍵新增到選擇器的已選擇鍵集以及從中將其移除,並且可以從其鍵集和已取消鍵集中將其移除。選擇是由
select()
、select(long)
和
selectNow()
方法執行的,執行涉及三個步驟:
-
將已取消鍵集中的每個鍵從所有鍵集中移除(如果該鍵是鍵集的成員),並登出其通道。此步驟使已取消鍵整合為空集。
-
在開始進行選擇操作時,應查詢基礎作業系統來更新每個剩餘通道的準備就緒資訊,以執行由其鍵的相關集合所標識的任意操作。對於已為至少一個這樣的操作準備就緒的通道,執行以下兩種操作之一:
-
如果該通道的鍵尚未在已選擇鍵集中,則將其新增到該集合中,並修改其準備就緒操作集,以準確地標識那些通道現在已報告為之準備就緒的操作。丟棄準備就緒操作集中以前記錄的所有準備就緒資訊。
-
如果該通道的鍵已經在已選擇鍵集中,則修改其準備就緒操作集,以準確地標識所有通道已報告為之準備就緒的新操作。保留準備就緒操作集以前記錄的所有準備就緒資訊;換句話說,基礎系統所返回的準備就緒操作集是和該鍵的當前準備就緒操作集按位分開 (bitwise-disjoined) 的。
-
-
如果在步驟 (2) 的執行過程中要將任意鍵新增到已取消鍵集中,則處理過程如步驟 (1)。
是否阻塞選擇操作以等待一個或多個通道準備就緒,如果這樣做的話,要等待多久,這是三種選擇方法之間的唯一本質差別。
併發性
選擇器自身可由多個併發執行緒安全使用,但是其鍵集並非如此。
選擇操作在選擇器本身上、在鍵集上和在已選擇鍵集上是同步的,順序也與此順序相同。在執行上面的步驟 (1) 和 (3) 時,它們在已取消鍵集上也是同步的。
在執行選擇操作的過程中,更改選擇器鍵的相關集合對該操作沒有影響;進行下一次選擇操作才會看到此更改。
可在任意時間取消鍵和關閉通道。因此,在一個或多個選擇器的鍵集中出現某個鍵並不意味著該鍵是有效的,也不意味著其通道處於開啟狀態。如果存在另一個執行緒取消某個鍵或關閉某個通道的可能性,那麼應用程式程式碼進行同步時應該小心,並且必要時應該檢查這些條件。
阻塞在 select()
或
select(long)
方法之一中的某個執行緒可能被其他執行緒以下列三種方式之一中斷:
close
方法在選擇器上是同步的,並且所有三個鍵集都與選擇操作中的順序相同。
一般情況下,選擇器的鍵和已選擇鍵集由多個併發執行緒使用是不安全的。如果這樣的執行緒可以直接修改這些鍵集之一,那麼應該通過對該鍵集本身進行同步來控制訪問。這些鍵集的
iterator
方法所返回的迭代器是快速失敗 的:如果在建立迭代器後以任何方式(呼叫迭代器自身的
remove
方法除外)修改鍵集,則會丟擲
ConcurrentModificationException
。
wakeup
public abstract Selector wakeup()
- 使尚未返回的第一個選擇操作立即返回。
如果另一個執行緒目前正阻塞在
select()
或select(long)
方法的呼叫中,則該呼叫將立即返回。如果當前未進行選擇操作,那麼在沒有同時呼叫selectNow()
方法的情況下,對上述方法的下一次呼叫將立即返回。在任一情況下,該呼叫返回的值可能是非零的。如果未同時再次呼叫此方法,則照常阻塞select()
或select(long)
方法的後續呼叫。在兩個連續的選擇操作之間多次呼叫此方法與只調用一次的效果相同。
-
- 返回:
- 此選擇器
selectNow
public abstract int selectNow() throws IOException
- 選擇一組鍵,其相應的通道已為 I/O 操作準備就緒。
此方法執行非阻塞的選擇操作。如果自從前一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回零。
呼叫此方法會清除所有以前呼叫
wakeup
方法所得的結果。 -
- 返回:
- 由選擇操作更新其準備就緒操作集的鍵的數目,該數目可能為零
- 丟擲:
select
public abstract int select() throws IOException
- 選擇一組鍵,其相應的通道已為 I/O 操作準備就緒。
此方法執行處於阻塞模式的選擇操作。僅在至少選擇一個通道、呼叫此選擇器的
wakeup
方法,或者當前的執行緒已中斷(以先到者為準)後此方法才返回。
一、 SelectionKey用完一定移除
Iterator<SelectionKey> it=selector.selectedKeys().iterator();
...for/while it.hasNext()...
it.remove();//鍵使用完成後必須這樣做,或者Set.clear()也行; 否則cpu佔用100%,特別是當客戶端切斷連線(channel)時 。
移除完成以後一定要重新做一下這個操作:
selectionKey.interestOps(selectionKey.interestOps() & (~selectionKey.readyOps));
將事件刪除,重新註冊
二、Selector.open()不是執行緒安全的,可能丟擲NullPointerException
synchronized (Selector.class) {
// Selector.open() isn't thread safe
// http://bugs.sun.com/view_bug.do?bug_id=6427854
// Affects 1.6.0_29, fixed in 1.7.0_01
SHARED_SELECTOR = Selector.open();
}
三、如果一個selection thread已經在select方法上等待,那麼這個時候如果有另一條執行緒呼叫channal.register方法的話,那麼它將被blocking.
四、selectionKey.cancel() BUG導致CPU佔用100%
其原因就是呼叫key.cancel()時底層在下一次seelect前並沒有真正的取消。導致等待select事件返回卻又沒有返回我們註冊的key.這個事件不斷地迴圈觸發,CPU一直處理返回 key為0的select()呼叫。解決方法有兩種,一是在key.cancel()後立即selectNow();但是如果是多執行緒併發操作,有可能這兩行語句中間執行緒被切換,使得key.cancel()後沒有立即執行selectNow().這在多Selector情況下是可能的。另一種就是jetty處理方式,如果select()返回0且連續幾次出現這樣的情況(有事件觸發返回,卻不是返回我們註冊的KEY),就將有效的key重新註冊到一個新的selector上。其實glassfish在處理多次次次次write返回為0的情況時也是這種策略。
tomcat7的處理方式:
try{
//對鍵的操作
}finally{
if (key != null) {
key.cancel();
if (selector != null)
selector.selectNow();// removes the key from this selector
}
}
Jetty的處理方式:
long before=now;
int selected=selector.select(wait);
now = System.currentTimeMillis();
_idleTimeout.setNow(now);
_timeout.setNow(now);
// Look for JVM bugs
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933
if (__JVMBUG_THRESHHOLD>0 && selected==0 && wait>__JVMBUG_THRESHHOLD && (now-before)<(wait/2) )
{
_jvmBug++;
if (_jvmBug>=(__JVMBUG_THRESHHOLD2))
{
synchronized (this)
{
_lastJVMBug=now;// BLOODY SUN BUG !!! Try refreshing the entire selector.
final Selector new_selector = Selector.open();
for (SelectionKey k: selector.keys())
{
if (!k.isValid() || k.interestOps()==0)
continue;
final SelectableChannel channel = k.channel();
final Object attachment = k.attachment();
if (attachment==null)
addChange(channel);
else
addChange(channel,attachment);
}
_selector.close();
_selector=new_selector;
_jvmBug=0;
return;
}
}
else if (_jvmBug==__JVMBUG_THRESHHOLD || _jvmBug==__JVMBUG_THRESHHOLD1)
{
// Cancel keys with 0 interested ops
for (SelectionKey k: selector.keys())
{
if (k.isValid()&&k.interestOps()==0)
{
k.cancel();
}
}
return;
}
}
else
_jvmBug=0;
五、(so) SocketChannels registered with OP_WRITE only release selector once (win) CPU 100%
總結:使用JDK 6 U4以上版本不會出現以上bug(除“Selector.open()不是執行緒安全的”以外)。