【從入門到放棄-Java】併發編程-NIO-Selector

前言

前兩篇【從入門到放棄-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線程

更多文章見:https://nc2era.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章