前言
前兩篇【從入門到放棄-Java】併發編程-NIO-Channel和【從入門到放棄-Java】併發編程-NIO-Buffer中我們學習了NIO中兩個重要的概念Channel和Buffer。
今天我們來看下另一個重要的內容 Selector
簡介
Selector是多路複用器,會不斷輪詢已經註冊了的Channel。當有註冊的channel產生連接、讀、寫等事件時,就會被Selector發現,從而可以進行相關後續操作。
Selector的好處是,可以通過一個線程來管理多個通道,減少了創建線程的資源佔用及線程切換帶來的消耗
Selector
SelectableChannel可以通過SelectionKey(記錄channel和selector的註冊關係)註冊到Selector上。Selector維護了三個SelectionKey集合:
- key set:存放了Selector上已經註冊了的Channel的key。可以通過keys()方法獲取。
- selected-key set:當之前註冊感興趣的事件到達時,set中的keys會被更新或添加,set中維護了當前至少有一個可以操作的事件的channel key的集合。是key set的子集。可以使用selectedKeys()獲取。
- cancelled-key:存放已經調用cancel方法取消,等待下次操作時會調用deregister取消註冊的channel,調用deregister後,所有的set中都沒有這個channel的key了。
open
/**
* Opens a selector.
*
* <p> The new selector is created by invoking the {@link
* java.nio.channels.spi.SelectorProvider#openSelector openSelector} method
* of the system-wide default {@link
* java.nio.channels.spi.SelectorProvider} object. </p>
*
* @return A new selector
*
* @throws IOException
* If an I/O error occurs
*/
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
開啓selector,具體的實現會根據操作系統類型有不同的實現類,如macOS下實際上是new了一個KQueueSelectorProvider實例
register
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
//新建一個SelectionKey,記錄channel與selector之間的註冊關係
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
//前置操作,這裏主要是判斷下selector是否還處於open狀態
// register (if needed) before adding to key set
implRegister(k);
// 添加selectionKey至key set
// add to the selector's key set, removing it immediately if the selector
// is closed. The key is not in the channel's key set at this point but
// it may be observed by a thread iterating over the selector's key set.
keys.add(k);
try {
// 更新註冊的事件碼
k.interestOps(ops);
} catch (ClosedSelectorException e) {
assert ch.keyFor(this) == null;
keys.remove(k);
k.cancel();
throw e;
}
return k;
}
註冊selector和channel之間的事件關係。
select
// timeout超時
@Override
public final int select(long timeout) throws IOException {
if (timeout < 0)
throw new IllegalArgumentException("Negative timeout");
return lockAndDoSelect(null, (timeout == 0) ? -1 : timeout);
}
@Override
public final int select() throws IOException {
return lockAndDoSelect(null, -1);
}
// 不阻塞
@Override
public final int selectNow() throws IOException {
return lockAndDoSelect(null, 0);
}
private int lockAndDoSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
synchronized (this) {
ensureOpen();
if (inSelect)
throw new IllegalStateException("select in progress");
inSelect = true;
try {
synchronized (publicSelectedKeys) {
return doSelect(action, timeout);
}
} finally {
inSelect = false;
}
}
}
protected int doSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
assert Thread.holdsLock(this);
// 如果timeout = 0時,不阻塞
long to = Math.min(timeout, Integer.MAX_VALUE); // max kqueue timeout
boolean blocking = (to != 0);
boolean timedPoll = (to > 0);
int numEntries;
processUpdateQueue();
processDeregisterQueue();
try {
// 設置interrupt 可以處理中斷信號 防止線程一直阻塞
begin(blocking);
// 輪詢的監聽,直到有註冊的事件發生或超時。
do {
long startTime = timedPoll ? System.nanoTime() : 0;
numEntries = KQueue.poll(kqfd, pollArrayAddress, MAX_KEVENTS, to);
if (numEntries == IOStatus.INTERRUPTED && timedPoll) {
// timed poll interrupted so need to adjust timeout
long adjust = System.nanoTime() - startTime;
to -= TimeUnit.MILLISECONDS.convert(adjust, TimeUnit.NANOSECONDS);
if (to <= 0) {
// timeout expired so no retry
numEntries = 0;
}
}
} while (numEntries == IOStatus.INTERRUPTED);
assert IOStatus.check(numEntries);
} finally {
end(blocking);
}
processDeregisterQueue();
return processEvents(numEntries, action);
}
selectedKeys
public final Set<SelectionKey> selectedKeys() {
ensureOpen();
return publicSelectedKeys;
}
獲取被事件喚醒的key
注意:當被遍歷處理selectedKeys時,key被處理完需要手動remove掉,防止下次被重複消費,selectedKeys不會幫你刪除已處理過的key。
close
public final void close() throws IOException {
boolean open = selectorOpen.getAndSet(false);
if (!open)
return;
implCloseSelector();
}
public final void implCloseSelector() throws IOException {
//通知處於阻塞的select方法立即返回
wakeup();
synchronized (this) {
implClose();
synchronized (publicSelectedKeys) {
// 遍歷所有的SelectionKey,取消註冊
// Deregister channels
Iterator<SelectionKey> i = keys.iterator();
while (i.hasNext()) {
SelectionKeyImpl ski = (SelectionKeyImpl)i.next();
deregister(ski);
SelectableChannel selch = ski.channel();
if (!selch.isOpen() && !selch.isRegistered())
((SelChImpl)selch).kill();
selectedKeys.remove(ski);
i.remove();
}
assert selectedKeys.isEmpty() && keys.isEmpty();
}
}
}
SelectionKey
SelectionKey在channel register時創建。用來記錄channel和selector之間的註冊事件關係。
事件主要有:
- OP_READ
- OP_WRITE
- OP_CONNECT
- OP_ACCEPT
每個SelectionKey有兩個由整數表示的操作集合,用來標識channel支持的操作類型。
interest set:是在創建SelectionKey時定義的,當集合中的操作發生時,將會把channel置爲ready狀態
ready set:檢測到selector中已經就緒的操作類型集合
channel
public SelectableChannel channel() {
return (SelectableChannel)channel;
}
獲取SelectionKey中的channel
selector
public Selector selector() {
return selector;
}
獲取SelectionKey中的selector
isReadable
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
根據readyOps(readySet)判斷channel是否是可讀狀態
isWritable
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
根據readyOps(readySet)判斷channel是否是可寫狀態
isConnectable
public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
}
根據readyOps(readySet)判斷channel是否是connect狀態,通常是客戶端使用,判斷連接是否建立
isReadable
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
根據readyOps(readySet)判斷channel是否是accept狀態,通常是服務端使用,判斷是否有客戶端請求建立連接
總結
通過使用selector,可以使用一個線程來管理多個連接。需要注意的一點是,通常讀、寫操作都是比較耗時的,爲了提高服務端的性能應該把Selector::select和read、write的具體處理邏輯在不同的線程中處理。
即:使用一個線程來進行select,只做分發。在獲取到就緒的SelectionKey後,通過線程池在不同的線程中處理讀寫操作。
通過學習完NIO相關的知識,我們可以很清楚的回答下面這個問題
- 問:基於BIO實現的server端,當建立100個連接時,需要多少個線程?基於NIO實現的呢?
- 答:基於BIO實現的server端,通常需要由一個線程accept,併爲每個新建立的連接創建一個線程去處理IO操作,因此需要1個accept線程+100個IO線程。
基於NIO實現的server端,使用Selector多路複用機制,由一個線程進行select,爲了提高併發可以使用線程池來處理IO操作,通常爲了發揮CPU的性能會創建(cpu核數 2)個線程來處理IO操作。因此需要1個select線程+cpu核數2個IO線程