Java NIO開發需要注意的陷阱(轉)


陷阱1:處理事件忘記移除key
在select返回值大於0的情況下,循環處理
Selector.selectedKeys集合,每處理一個必須從Set中移除

Iterator<SelectionKey> it=set.iterator();
    While(it.hasNext()){
    SelectionKey key=it.next();
    it.remove(); //切記移除
    „„處理事件
}

 不移除的后果是本次的就緒的key集合下次會再次返回,導致無限循環,CPU消耗100%

 陷阱2:Selector返回的key集合非線程安全

Selector.selectedKeys/keys 返回的集合都是非線程安全的
Selector.selectedKeys返回的可移除
Selector.keys 不可變
對selected keys的處理必須單線程處理或者適當同步

陷阱3:正確注冊Channel和更新interest
直接注冊不可嗎?
channel.register(selector, ops, attachment);
不是不可以,效率問題
至少加兩次鎖,鎖競爭激烈
Channel本身的regLock,競爭幾乎沒有
Selector內部的key集合,競爭激烈
更好的方式:加入緩沖隊列,等待注冊,reactor單線程處理

If(isReactorThread()){
    channel.register(selector,ops,attachment);
}
else{
    register.offer(newEvent(channel,ops,attachment));
    selector.wakeup();
}

同樣,SelectionKey.interest(ops)
在linux上會阻塞,需要獲取selector內部鎖做同步
在win32上不會阻塞
屏蔽平台差異,避免鎖的激烈競爭,采用類似注冊channel的方式:

if (this.isReactorThread()) {
    key.interestOps(key.interestOps() | SelectionKey.OP_READ);
} 
else {
    this.register.offer(new Event(key,SelectionKey.OP_READ));
    selector.wakeup();
}

 

陷阱4:正確處理OP_WRITE
OP_WRITE處理不當很容易導致CPU 100%
OP_WRITE觸發條件:
   前提:interest了OP_WRITE
   觸發條件:
        socket發送緩沖區可寫
        遠端關閉
        有錯誤發生
正確的處理方式:
   僅在已經連接的channel上注冊
   僅在有數據可寫的時候才注冊
   觸發之后立即取消注冊,否則會繼續觸發導致循環
   處理完成后視情況決定是否繼續注冊
     沒有完全寫入,繼續注冊
     全部寫入,無需注冊

陷阱5:正確取消注冊channel
SelectableChannel一旦注冊將一直有效直到明確取消
怎么取消注冊?
   channel.close(),內部會調用key.cancel()
   key.cancel();
   中斷channel的讀寫所在線程引起的channel關閉
但是這樣還不夠!
   key.cancel()僅僅是將key加入cancelledKeys
   直到下一次select才真正處理
   並且channel的socketfd只有在真正取消注冊后才會close(fd)

后果是什么?
  服務端,問題不大,select調用頻繁
  客戶端,通常只有一個連接,關閉channel之后,沒有調用select就關閉了selector
  sockfd沒有關閉,停留在CLOSE_WAIT狀態
正確的處理方式,取消注冊也應當作為事件交給reactor處理,及時wakeup做select
適當的時候調用selector.selectNow()
  Netty在超過256連接關閉的時候主動調用一次selectNow

static final int CLEANUP_INTERVAL=256;
private boolean cleanUpCancelledKeys()throws IOException{
    if(cancelledKeys>=CLEANUP_INTERVAL){
        cancelledKeys=0;
        selector.selectNow();
        returntrue;
    }
    returnfalse;
}
//channel關閉的時候
channel.socket.close();
cancelledKeys++;

陷阱6:同時注冊OP_ACCPET和OP_READ,同時注冊OP_CONNECT和OP_WRITE
在底層來說,只有兩種事件:read和write
Java NIO還引入了OP_ACCEPT和OP_CONNECT
  OP_ACCEPT、OP_READ == Read
  OP_CONNECT、OP_WRITE == Write
同時注冊OP_ACCEPT和OP_READ ,或者同時注冊OP_CONNECT和OP_WRITE在不同平台上產生錯誤的行為,避免這樣做!

陷阱7:正確處理connect
SocketChannel.connect方法在非阻塞模式下可能返回false,切記判斷返回值
    如果是loopback連接,可能直接返回true,表示連接成功
    返回false,后續處理
       注冊channel到selector,監聽OP_CONNECT事件
       在OP_CONNECT觸發后,調用SocketChannel.finishConnect成功后,連接才真正建立
陷阱:
    沒有判斷connect返回值
    沒有調用finishConnect
    在OP_CONNECT觸發后,沒有移除OP_CONNECT,導致SelectionKey一直處於就緒狀態,空耗CPU
       OP_CONNECT只能在還沒有連接的channel上注冊

忠告

盡量不要嘗試實現自己的nio框架,除非有經驗豐富的工程師
盡量使用經過廣泛實踐的開源NIO框架Mina、Netty3、xSocket
盡量使用最新穩定版JDK
遇到問題的時候,也許你可以先看下java的bug database


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM