自頂向下深入分析Netty(四)--EventLoop-2

4.4 線程

4.4.1 AbstractExecutorService

AbstractExecutorService是JDK并發包中的類,實現了ExecutorService中的submit()和invoke***()方法,關鍵實現是其中的newTaskFor()方法,使用FutureTask包裝一個Ruannble對象和結果或者一個Callable對象。注意,這個方法是一個protected方法,子類中可以覆蓋這個實現。

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
    
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

4.4.2 AbstractEventExecutor

AbstractEventExecutor繼承自AbstractExecutorService并實現了EventExecutor接口,該類中只實現了一些簡單的方法:

    public EventExecutor next() {
        return this;
    }    

    public boolean inEventLoop() {
        return inEventLoop(Thread.currentThread());
    }
    
    public Future<?> shutdownGracefully() {
        return shutdownGracefully(2, 15, TimeUnit.SECONDS);
    }

next()方法在線程池的講解中已經接觸過,功能是選擇線程池中的一個線程,將AbstractEventExecutor看為只有一個線程的線程池,所以next()返回它本身。inEventLoop()和shutdownGracefully()方法都調用它的有參方法,我們將在其子類實現中詳細介紹,這里我們先了解其功能即可。inEventLoop()的功能使判斷當前線程是否是EventExecutor原生線程,shutdownGracefully()即優雅關閉。
AbstractEventExecutor類中有四個創建異步結果的方法,實現類似如下:

    public <V> Promise<V> newPromise() {
        return new DefaultPromise<V>(this);
    }

AbstractEventExecutor類覆蓋了父類的newTaskFor()方法:

    @Override
    protected final <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new PromiseTask<T>(this, callable);
    }

使用Netty的PromiseTask代替JDK的FutureTask,其中的差別,我們在下一節講述。此外,還用Netty的Future對象覆蓋了subimt()方法的返回值(原本為JDK的Future).

4.4.3 AbstractScheduledEventExecutor

從名字可以看出,AbstractScheduledEventExecutor類是關于Schedule的實現。如果要調度一堆任務,那么首先要有存放任務的容器,Netty中使用隊列:

    Queue<ScheduledFutureTask<?>> scheduledTaskQueue;
    
    Queue<ScheduledFutureTask<?>> scheduledTaskQueue() {
        if (scheduledTaskQueue == null) {
            scheduledTaskQueue = new PriorityQueue<ScheduledFutureTask<?>>();
        }
        return scheduledTaskQueue;
    }

該調度任務隊列是一個優先級隊列,并使用了延遲加載。其核心的調度方法實現如下:

    <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
        if (inEventLoop()) {
            scheduledTaskQueue().add(task); // 原生線程直接向任務隊列添加
        } else {
            execute(new Runnable() {    // 其他線程則提交一個添加調度任務的任務
                @Override
                public void run() {
                    scheduledTaskQueue().add(task);
                }
            });
        }
        return task;
    }

可以看出實現很簡單,就是向調度任務隊列中添加一個任務,為了弄明白具體的調度過程,我們需要明白ScheduledFutureTask,下面我們將詳細介紹。

ScheduledFutureTask

首先看其中的靜態字段和靜態方法:

    // 調度任務ID生成器
    private static final AtomicLong nextTaskId = new AtomicLong();
    // 調度相對時間起點
    private static final long START_TIME = System.nanoTime();
    
    // 獲取相對的當前時間
    static long nanoTime() {
        return System.nanoTime() - START_TIME;
    }

    // 獲取相對的截止時間
    static long deadlineNanos(long delay) {
        return nanoTime() + delay;
    }

注意:Netty使用了相對時間調度,時間起點為ScheduledFutureTask類第一次被類加載器加載的時間。
然后我們看其中的私有字段:

    // 調度任務ID
    private final long id = nextTaskId.getAndIncrement();
    // 調度任務截止時間即到了改時間點任務將被執行
    private long deadlineNanos;
    // 任務時間間隔
    private final long periodNanos;

這里的periodNanos字段還兼有標記的功能,0--表示調度任務不重復,>0--表示按固定頻率重復(at fixed rate),<0--表示按固定延遲重復(with fixed delay)。這不是一個好的設計,但也沒有暴露給用戶程序員,算一個折中處理。
接著我們看關鍵的run()方法:

    @Override
    public void run() {
        assert executor().inEventLoop();
        try {
            if (periodNanos == 0) { // 普通不重復的調度任務直接執行
                if (setUncancellableInternal()) {
                    V result = task.call();
                    setSuccessInternal(result);
                }
            } else {
                if (!isCancelled()) {   // 重復的任務可能被取消
                    task.call();
                    if (!executor().isShutdown()) { // 線程已經關閉則不再添加新任務
                        long p = periodNanos;
                        if (p > 0) {
                            deadlineNanos += p; // 按固定頻率重復
                        } else {
                            deadlineNanos = nanoTime() - p; // 按固定延遲重復
                        }
                        if (!isCancelled()) {
                            Queue<ScheduledFutureTask<?>> scheduledTaskQueue =
                                    ((AbstractScheduledEventExecutor) executor()).scheduledTaskQueue;
                            assert scheduledTaskQueue != null;
                            scheduledTaskQueue.add(this); // 下一個最近的重復任務添加到任務隊列
                        }
                    }
                }
            }
        } catch (Throwable cause) {
            setFailureInternal(cause);
        }
    }

代碼中的注釋很好的解釋了一個調度任務的執行過程,可能你會對按固定延遲重復的任務有疑問,即:

    deadlineNanos = nanoTime() - p;

其中nanoTime()指當前時間(注意是相對時間),由于p是負值-p等價于:當前時間+delay時間。由于ScheduledFutureTask是添加到PriorityQueue中的對象,我們再看看其中的compareTo()方法:

    @Override
    public int compareTo(Delayed o) {
        if (this == o) {
            return 0;
        }
        ScheduledFutureTask<?> that = (ScheduledFutureTask<?>) o;
        long d = deadlineNanos() - that.deadlineNanos();
        if (d < 0) {
            return -1;
        } else if (d > 0) {
            return 1;
        } else if (id < that.id) {
            return -1;
        } else if (id == that.id) {
            throw new Error();
        } else {
            return 1;
        }
    }

從代碼可以看出,優先級隊列的出隊順序是:截止時間最近的先出隊,如果截止時間相同則ID小的先出隊。
分析完ScheduledFutureTask類,我們接著分析AbstractScheduledEventExecutor類中剩下的方法,由于其中的方法實現簡單明了,不再列出代碼實現,只列出其方法簽名:

    // 返回當前時間(相對時間)
    protected static long nanoTime() {
        return ScheduledFutureTask.nanoTime(); // 使用ScheduledFutureTask的相對時間
    }
    // 取得并移除截止時間大于nanoTime的下一個調度任務
    protected final Runnable pollScheduledTask(long nanoTime);
    // 取得距離下一個調度任務執行的間隔時間
    protected final long nextScheduledTaskNano();
    // 取得但并不移除下一個調度任務
    final ScheduledFutureTask<?> peekScheduledTask();
    // 是否有將要執行的調度任務
    protected final boolean hasScheduledTasks();
    // 刪除一個調度任務
    final void removeScheduled(final ScheduledFutureTask<?> task);

4.4.4 SingleThreadEventExecutor

SingleThreadEventExecutor類從名字可以看出,它是一個單線程的Executor實現。在介紹之前,我們先看Netty定義的線程狀態:

    private static final int ST_NOT_STARTED = 1;    // 沒有啟動
    private static final int ST_STARTED = 2;    // 啟動
    private static final int ST_SHUTTING_DOWN = 3;  // 正在關閉
    private static final int ST_SHUTDOWN = 4;   // 關閉
    private static final int ST_TERMINATED = 5; // 終止

需要注意的有兩點:
(1).本類的實現中線程采用延遲啟動(lazy start),只有當提交第一個任務時線程才啟動,從而節省資源。
(2).當調用shutdownGracefully()時,線程狀態改變為ST_SHUTTING_DOWN;調用shutdown()時,線程狀態改變為ST_SHUTDOWN。
明白了線程狀態,我們首先看一下類中的字段:

    private final EventExecutorGroup parent;    // 該Executor所屬的線程池
    private final Queue<Runnable> taskQueue;    // 任務隊列
    private final Thread thread;    // 改Executor所屬的線程
    private final ThreadProperties threadProperties;    // 線程屬性值
    private final Semaphore threadLock = new Semaphore(0);  // 一個信號量,注意初始值為0
    private final Set<Runnable> shutdownHooks = new LinkedHashSet<~>(); // 線程關閉鉤子任務
    private final boolean addTaskWakesUp;   // 添加任務時是否喚醒線程
    private final int maxPendingTasks;  // 任務隊列大小即未執行的最大任務數
    private final RejectedExecutionHandler rejectedExecutionHandler;    // 隊列滿時的阻止器

    private long lastExecutionTime; // 上一次執行時間

    private volatile int state = ST_NOT_STARTED;    // 線程狀態,注意該字段由STATE_UPDATER修改

    // 線程終止異步結果
    private final Promise<?> terminationFuture = new DefaultPromise<Void>(
                                                            GlobalEventExecutor.INSTANCE);

關于SingleThreadEventExecutor的構造方法,我們摘選下面的關鍵代碼:

    thread = threadFactory.newThread(() -> {
                updateLastExecutionTime();
                try {
                    SingleThreadEventExecutor.this.run();   // 這是一個模板方法
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                } finally {
                    // shutdown
                }
    });
    taskQueue = newTaskQueue(); // 這里使用該方法是為了子類可以優化

其中使用了模板方法run(),由子類負責實現。taskQueue也由一個方法實例,主要是給子類提供一個優化的機會,關于Netty的優化,我們以后將專門講解,這里taskQueue的默認實現是LinkedBlockingQueue。
下面我們分析一個關鍵方法runAllTasks(long timeoutNanos),其功能是用給定的timeoutNanos時間執行任務隊列中的任務,代碼如下:

    protected boolean runAllTasks(long timeoutNanos) {
        fetchFromScheduledTaskQueue();  // 將調度任務隊列中到期的任務移到任務隊列
        Runnable task = pollTask(); // 從任務隊列頭部取出一個任務
        if (task == null) {
            return false;
        }

        final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos; // 執行截止時間
        long runTasks = 0;
        long lastExecutionTime;
        for (;;) {
            try {
                task.run();
            } catch (Throwable t) {
                logger.warn("A task raised an exception.", t);
            }
            runTasks ++;
            // 每執行64個任務檢查時候時間已到截止時間,0x3F = 64-1
            if ((runTasks & 0x3F) == 0) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                if (lastExecutionTime >= deadline) {
                    break;
                }
            }

            task = pollTask();
            if (task == null) { // 沒有任務則退出
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                break;
            }
        }
        // 更新上一次執行時間
        this.lastExecutionTime = lastExecutionTime;
        return true;
    }

我們再看一下fetchFromScheduledTaskQueue()方法,它從調度任務隊列取出所有到期的調度任務并加入到任務隊列,除非任務隊列滿,代碼如下:

    private boolean fetchFromScheduledTaskQueue() {
        // 等價于ScheduledFutureTask.nanoTime()
        long nanoTime = AbstractScheduledEventExecutor.nanoTime(); 
        Runnable scheduledTask  = pollScheduledTask(nanoTime);
        while (scheduledTask != null) {
            if (!taskQueue.offer(scheduledTask)) {
                // 任務隊列已滿,則重新添加到調度任務隊列
                scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
                return false;
            }
            scheduledTask  = pollScheduledTask(nanoTime);
        }
        return true;
    }

runAllTasks()還有一個無參方法,其功能將所有到期的調度任務從調度任務隊列移入任務隊列,并執行任務隊列中的所有任務(包括非調度任務),我們不再列出代碼。
SingleThreadEventExecutor類是一個通用框架,不僅可以執行異步任務,也能執行同步任務,下面我們分析其中用于執行同步任務的關鍵方法takeTask(),其功能是取出任務隊列頭部的任務,如果沒有任務則會一直阻塞,代碼如下:

    protected Runnable takeTask() {
        assert inEventLoop();
        if (!(taskQueue instanceof BlockingQueue)) {  // 任務隊列必須是阻塞隊列
            throw new UnsupportedOperationException();
        }

        BlockingQueue<Runnable> taskQueue = (BlockingQueue<Runnable>) this.taskQueue;
        for (;;) {
            // 取得調度任務隊列的頭部任務,注意peek并不移除
            ScheduledFutureTask<?> scheduledTask = peekScheduledTask(); 
            if (scheduledTask == null) { // 沒有調度任務
                Runnable task = null;
                try {
                    task = taskQueue.take();  // 取得并移除任務隊列的頭部任務,沒有則阻塞
                    if (task == WAKEUP_TASK) {
                        task = null;
                    }
                } catch (InterruptedException e) {
                    // Ignore
                }
                return task;
            } else {
                long delayNanos = scheduledTask.delayNanos(); // 調度任務的到期時間間隔
                Runnable task = null;
                if (delayNanos > 0) {
                    try {   // 調度任務未到期,則從任務隊列取一個任務,可能為null
                        task = taskQueue.poll(delayNanos, TimeUnit.NANOSECONDS);
                    } catch (InterruptedException e) {
                        return null;
                    }
                }
                
                // 注意這里執行有兩種情況:1.任務隊列中沒有待執行任務,2.調度任務已到期
                if (task == null) {
                    fetchFromScheduledTaskQueue();
                    task = taskQueue.poll();
                }

                if (task != null) {
                    return task;
                }
            }
        }
    }

特別關注一下15行代碼,這里有一個WAKEUP_TASK,它是一個標記任務。使用這個標記任務是為了線程能正確退出,當線程需要關閉是,如果線程在take()方法上阻塞,就需要添加一個標記任務WAKEUP_TASK到任務隊列,是線程從take()返回,從而正確關閉線程。

    protected void wakeup(boolean inEventLoop) {
        if (!inEventLoop || STATE_UPDATER.get(this) == ST_SHUTTING_DOWN) {
            // 非本類原生線程或者本類原生線程需要關閉時,添加一個標記任務使線程從take()返回。
            // offer失敗表明任務隊列已有任務,從而線程可以從take()返回故不處理
            taskQueue.offer(WAKEUP_TASK);
        }
    }

本類覆蓋了execute()方法,在這里實現了線程的延遲啟動(lazy start),代碼如下:

    public void execute(Runnable task) {
        boolean inEventLoop = inEventLoop();
        if (inEventLoop) {  // 原生線程直接添加
            addTask(task);  
        } else {    // 外部線程啟動線程后添加
            startThread();
            addTask(task);
            if (isShutdown() && removeTask(task)) {
                reject();   // 原生線程關閉時則阻止添加,拋出異常
            }
        }
        // 是否喚醒線程,addTaskWakesUp由構造方法配置,wakesUpForTask()可由子類覆蓋,默認喚醒
        // 這里這個參數值addTaskWakesUp和其說明有出入,現在false反而喚醒?
        if (!addTaskWakesUp && wakesUpForTask(task)) {
            wakeup(inEventLoop);
        }
    }

Netty線程關閉的代碼較為繁瑣,我們先不列出,以后專門使用一節講述。此外,本類中其他需要說明的方法,我們列出方法簽名和說明:

    // 取得并移除任務隊列的頭部任務,忽略WAKEUP_TASK標記任務
    protected Runnable pollTask();
    // 取得任務隊列的頭部任務
    protected Runnable peekTask();
    // 任務隊列是否有任務即是否為空
    protected boolean hasTasks();
    // 掛起的任務數即任務隊列大小
    public int pendingTasks();
    // 添加一個任務,線程關閉時拋出異常
    protected void addTask(Runnable task);
    final boolean offerTask(Runnable task);
    // 移除一個任務
    protected boolean removeTask(Runnable task);
    // 下一個調度任務到期的時間間隔
    protected long delayNanos(long currentTimeNanos);
    
    // 判斷線程是否為該類的原生線程
    public boolean inEventLoop(Thread thread) {
        return thread == this.thread;
    }

4.4.5 SingleThreadEventLoop

SingleThreadEventLoop終于與Channel取得聯系,其中最重要的便是register()方法,功能是將一個Channel對象注冊到EventLoop上,其最終實現委托Channel對象的Unsafe對象完成,關于Unsafe我們將在下一章介紹。其代碼實現如下:

    @Override
    public ChannelFuture register(Channel channel) {
        return register(channel, new DefaultChannelPromise(channel, this));
    }

    @Override
    public ChannelFuture register(final Channel channel, final ChannelPromise promise) {
        // 代碼中省略了NullPointer檢查
        channel.unsafe().register(this, promise);
        return promise;
    }

該類還覆蓋了父類的wakesUpForTask(Runnable task)方法,實現如下:

    @Override
    protected boolean wakesUpForTask(Runnable task) {
        return !(task instanceof NonWakeupRunnable);
    }
    
    // 標記接口,用于標記不喚醒原生線程的任務
    interface NonWakeupRunnable extends Runnable { }

4.4.6 NioEventLoop

前面鋪墊了這么多,終于到了我們的目的地NioEventLoop。NioEventLoop的功能是對注冊到其中的Channnel的就緒事件以及對用戶提交的任務進行處理,回憶第一章關于Java NIO的講解,NioEventLoop正是要完成第一章中所示的代碼的工作。首先我們從其中的字段開始:

    Selector selector;  // NIO中的多路復用器Selector
    private SelectedSelectionKeySet selectedKeys;   // 就緒事件的鍵值對,優化時使用
    private final SelectorProvider provider;    // selector的工廠
    // 喚醒標記,由于select()方法會阻塞
    private final AtomicBoolean wakenUp = new AtomicBoolean(); 
    private final SelectStrategy selectStrategy; // 選擇策略
    private volatile int ioRatio = 50;  // IO任務占總任務(IO+普通任務)比例
    private int cancelledKeys;  // 取消的鍵數目
    private boolean needsToSelectAgain; 

在講解方法前,我們再回顧一下NioEventLoop的繼承體系:
(1).JDK的AbstractExecutorService類定義了任務的提交和執行,留下了newTaskFor()方法用于子類定義執行的任務;
(2).Netty的AbstractEventExecutor類覆蓋了newTaskFor()方法,使用PromiseTask表示待執行的任務;
(3).AbstractScheduledEventExecutor類將待執行的調度任務封裝為ScheduledFutureTask提交給調度任務隊列;
(4).SingleThreadEventExecutor類實現了任務執行器即線程,其覆蓋了execute()方法,當使用execute()執行一個任務時,實質是向任務隊列提交一個任務;該類中還有一個重要的模板方法run(),在這個方法中執行任務隊列中的任務(調度任務隊列中的待執行任務移入普通任務隊列),留給子類實現;
(5).SingleThreadEventLoop類實現對Channel對象的注冊。
從NioEventLoop繼承體系的分析可以看出,NioEventLoop要實現的最關鍵方法就是基類的模板方法run()。是不是已經迫不及待了?好,我們直奔代碼:

    @Override
    protected void run() {
        for (;;) { 
            try {
                // 調用select()查詢是否有就緒的IO事件
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));
                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                    default:
                        // fallthrough
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    processSelectedKeys();  // 處理就緒的IO事件
                    runAllTasks();  // 執行完任務隊列中的任務
                } else {
                    final long ioStartTime = System.nanoTime();
                    processSelectedKeys();  // 處理就緒的IO事件
                    final long ioTime = System.nanoTime() - ioStartTime;
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);    // 給定時間內執行任務
                }

                if (isShuttingDown()) { // 檢測用戶是否要終止線程
                    closeAll();
                    if (confirmShutdown()) {
                        break;
                    }
                }
            } catch (Throwable t) {
                logger.warn("Unexpected exception in the selector loop.", t);
                try {
                    Thread.sleep(1000); // 防止連續異常過度消耗CPU
                } catch (InterruptedException e) {
                    // Ignore.
                }
            }
        }
    }

從代碼中可以看出NioEventLoop完成了三項任務:
(1).輪訓Channel選擇就緒的IO事件。
(2).處理就緒的IO事件。
(3).處理任務隊列中的普通任務(包含調度任務)。
其中第(3)項,我們已經在SingleThreadEventExecutor類中分析過,不再贅述。我們看代碼的6-16行即第(1)項,輪詢Channel選擇就緒的IO事件。這里使用接口SelectStrategy是用戶可以選擇具體的選擇策略,我們主要看默認實現:

    @Override
    public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
        return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
    }
    
    private final IntSupplier selectNowSupplier = () -> { return selectNow(); };

故默認策略是:如果有普通任務待執行,使用selectNow();否則使用select(boolean oldWakenUp)。NIO的Selector有三個select()方法,它們的區別如下:

select() 阻塞直到有一個感興趣的IO事件就緒
select(long timeout) 與select()類似,但阻塞的最長時間為給定的timeout
selectNow() 不會阻塞,直接返回而不管是否有IO事件就緒

此外,還有一個重要的wakeUp()方法,其功能是喚醒一個阻塞在select()上的線程,使其繼續運行。如果先調用了wakeUp()方法,那么下一個select()操作也會立即返回。此外,wakeUp()是一個昂貴的方法,應盡量減少其調用次數。
有了這些基礎知識,我們看本類中與selec()操作有關的方法,首先看selecNow()方法:

    int selectNow() throws IOException {
        try {
            return selector.selectNow();
        } finally {
            if (wakenUp.get()) {    // wakenUp標記字段為真時,喚醒下一次select()操作
                selector.wakeup();
            }
        }
    }

實現也很簡單,我們主要看select(boolean oldWakenUp)方法:

    private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            // delayNanos返回的是最近的一個調度任務的到期時間,沒有調度任務返回1秒
            // selectDeadLineNanos指可以進行select操作的截止時間點
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
            for (;;) {
                // 四舍五入將select操作時間換算為毫秒單位
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                if (timeoutMillis <= 0) {   // 時間不足1ms,不再進行select操作
                    if (selectCnt == 0) {   // 如果一次select操作沒有進行
                        selector.selectNow();   // selecNow()之后返回
                        selectCnt = 1;
                    }
                    break;
                }

                // 此時有任務進入隊列且喚醒標志為假
                if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                    selector.selectNow(); // selectNow()返回,否則會耽誤任務執行
                    selectCnt = 1;
                    break;
                }

                int selectedKeys = selector.select(timeoutMillis);  
                selectCnt ++;

                // 有就緒的IO事件,參數oldWakenUp為真,外部設置wakenUp為真
                // 有待執行普通任務,有待執行調度任務
                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || 
                                                                        hasScheduledTasks()) {
                    break;
                }

                long time = System.nanoTime();
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                    selectCnt = 1;  // 截止時間已到(這里可直接break退出)
                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    rebuildSelector();  // 這里是對JDK BUG的處理
                    selector = this.selector;
                    selector.selectNow(); // 重建selector之后立即selectNow()
                    selectCnt = 1;
                    break;
                }
                currentTimeNanos = time;
            }
        } catch (CancelledKeyException e) {
        }
    }

本來select操作的代碼不會這么復雜,主要是由于JDK BUG導致select()方法并不阻塞而直接返回且返回值為0,從而出現空輪詢使CPU完全耗盡。Netty解決的辦法是:對select返回0的操作計數,如果次數大于閾值SELECTOR_AUTO_REBUILD_THRESHOLD就新建一個selector,將注冊到老的selector上的channel重新注冊到新的selector上。閾值SELECTOR_AUTO_REBUILD_THRESHOLD可由用戶使用系統變量io.netty.selectorAutoRebuildThreshold配置,默認為512。這里注意for()循環中大量使用了break,含有break的部分才是關鍵操作,其他部分(其實就只有一處)是為了解決JDK BUG。
為了完全理解這段代碼,我們還將講解一下wakeUp()方法,注意其中的21行和32行代碼。回憶一下SingleThreadEventExecutor的execute()方法,其最后有一個wakeUp()方法,作用是添加一個任務后指示是否需要喚醒線程。在NioEventLoop中覆蓋了它的實現:

    @Override
    protected void wakeup(boolean inEventLoop) {
        // 外部線程且喚醒標記為假時喚醒
        if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
            selector.wakeup(); // 注意此時喚醒標記為真
        }
    }
    
    select(wakenUp.getAndSet(false)); // run方法調用時

當run方法調用select()方法時,每次都將喚醒標記設置為假,這樣線程將阻塞在selector.select(timeoutMillis)方法上。阻塞期間如果用戶使用外部線程提交一個任務,會調用上述的wakeup()方法,由于wakenUp喚醒標記為假,selector.wakeup()方法調用,線程喚醒從下一個break跳出,從而執行提交任務。阻塞期間如果外部線程提交多個任務,使用wakenUp喚醒標記使selector.wakeup()操作只執行一次,因為它是一個昂貴的操作,從而提高性能。21行代碼進入if執行的前提是有任務且wakenUp喚醒標記為假,如果喚醒標記為真是什么情況呢?那說明由外部線程調用了selector.wakeup()方法,此時下一個select()操作會直接返回,繼而從下一個break返回,所以也不會影響已有任務的執行。在run()方法select之后的操作還有這樣兩行代碼:

    if (wakenUp.get()) {
        selector.wakeup();
    }

根據注釋的解釋是:在select(wakenUp.getAndSet(false))操作set(false)和selector.select(timeout)之間如果有外部線程將喚醒標記wakenUp設置為真且執行selector.wakeup()方法,則selector.select(timeout)的第一個操作立即返回,然后會阻塞在第二次循環的select.select(timeout)方法上,此時喚醒標記wakenUp為真從而阻止外部線程添加任務時喚醒線程,從而造成不必要的阻塞操作。(但是代碼在select(timeout)之后的一行使用了hasTasks()判斷,如果外部線程提交了任務也能跳出循環。所以這部分代碼和注釋是不是已失效?)
分析完select操作之后,我們接著分析Netty對IO事件的處理方法processSelectedKeys():

    private void processSelectedKeys() {
        if (selectedKeys != null) {
            processSelectedKeysOptimized(selectedKeys.flip());  // 使用優化
        } else {
            processSelectedKeysPlain(selector.selectedKeys());  // 普通處理
        }
    }

關于優化,我們將在專門的章節講述,我們先看普通處理:

    private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
        if (selectedKeys.isEmpty()) {
            return; // 選擇鍵的集合為空直接返回
        }

        Iterator<SelectionKey> i = selectedKeys.iterator();
        for (;;) {
            final SelectionKey k = i.next();
            final Object a = k.attachment();
            i.remove();

            if (a instanceof AbstractNioChannel) {  // IO事件由Netty框架處理
                processSelectedKey(k, (AbstractNioChannel) a);  
            } else {    // IO事件由用戶自定義任務處理
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);
            }

            if (!i.hasNext()) {
                break;
            }

            if (needsToSelectAgain) {
                selectAgain();
                selectedKeys = selector.selectedKeys();
                if (selectedKeys.isEmpty()) {
                    break;
                } else {
                    i = selectedKeys.iterator();
                }
            }
        }
    }

這一部分代碼功能就是遍歷選擇鍵,其中對選擇鍵的處理有兩種方式:Netty框架處理和用戶自定義處理。這兩種處理方式由register()方式決定:

    // Netty框架處理
    public ChannelFuture register(final Channel channel, final ChannelPromise promise);
    // 用戶自定義處理
    public void register(final SelectableChannel ch, final int interestOps, final NioTask<?> task);

注意23-31行代碼,什么時候需要再次執行select()操作呢?當取消的選擇鍵達到一定數目時,這個數目在Netty中時CLEANUP_INTERVAL,值為256。也就是每取消256個選擇鍵,Netty重新執行一個selectAgain()操作。這個操作實現使用selector.selectNow()并將needsToSelectAgain標記設置為假。cancle()代碼如下:

    void cancel(SelectionKey key) {
        key.cancel();
        cancelledKeys ++;
        if (cancelledKeys >= CLEANUP_INTERVAL) {
            cancelledKeys = 0;
            needsToSelectAgain = true;
        }
    }

接著分析最為關鍵的processSelectedKey()方法:

    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final NioUnsafe unsafe = ch.unsafe();
        if (!k.isValid()) { // 選擇鍵不再有效
            final EventLoop eventLoop;
            try {
                eventLoop = ch.eventLoop(); 
            } catch (Throwable ignored) {
                return;
            }
            // channel已不再該EventLoop,直接返回
            if (eventLoop != this || eventLoop == null) {
                return; 
            }
            // channel還在EventLoop,關閉channel
            unsafe.close(unsafe.voidPromise());
            return;
        }

        try {
            int readyOps = k.readyOps();
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {    // 客戶端連接事件
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);     // 連接完成后客戶端除了連接事件都感興趣
                unsafe.finishConnect();     // 完成連接
            }
            
            // readyOps == 0為對JDK Bug的處理, 防止死循環
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();  // 讀事件以及服務端的Accept事件都抽象為read()事件
                if (!ch.isOpen()) {
                    return;
                }
            }
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {  // 寫事件
                ch.unsafe().forceFlush();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

可以看出對IO事件的具體處理,委托給NioUnsafe對象處理,由read()、forceFlush()、finishConnect()和close()方法處理具體的IO事件,具體的處理過程,我們將在分析NioUnsafe時講解。
目前為止,我們已經講解完了NioEventLoop實現的最關鍵部分,當然還有一些細節我們需要完善:

    protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
        return PlatformDependent.newMpscQueue(maxPendingTasks);
    }

NioEventLoop由于不使用takeTask()方法,所以使用一個MPSC隊列代替基類的LinkedBlockingQueue作為新的任務隊列,大大提高了性能。如果你對MPSC(多個生產者一個消費者)隊列感興趣,可自行查看相關資料。

    @Override
    public int pendingTasks() {
        if (inEventLoop()) {
            return super.pendingTasks();
        } else {
            return submit(pendingTasksCallable).syncUninterruptibly().getNow(); // 同步等待結果
        }
    }
    
    private final Callable<Integer> pendingTasksCallable = () -> {
            return NioEventLoop.super.pendingTasks();
    };

這一部分代碼是使用MPSC隊列的副作用,由于MPSC只能由NioEventLoop原生線程訪問,否則會發生一些意外情況,所以查詢隊列大小,也向任務隊列提交一個任務同時同步等待結果。

    @Override
    protected Runnable pollTask() {
        Runnable task = super.pollTask();
        if (needsToSelectAgain) {
            selectAgain();
        }
        return task;
    }

NioEventLoop覆蓋了pollTask()的實現,在適當時機執行selector.selectNow()操作。(由于pollTask是在執行普通任務時調用,是否有必要?就算selectNow()有結果也不能處理)
Netty作為一個優化狂魔,將優化做到了極致。回憶處理選擇鍵的事件時,需要遍歷其存儲容器selectedKeySet,這是一個HashSet,迭代性能不高,那么優化。Netty使用新的SelectedSelectionKeySet代替JDK的HashSet,具體怎么實現的呢?在方法openSelector()中實現,代碼不在列出,其思路是:使用反射替換這個容器。
下面我們分析SelectedSelectionKeySet,首先看字段:

    private SelectionKey[] keysA;
    private int keysASize;
    private SelectionKey[] keysB;
    private int keysBSize;
    private boolean isA = true;     // 標記字段,控制使用具體的數組

可以看出SelectedSelectionKeySet使用雙數組實現,為什么要這樣設計呢?
(1).使用數組提高遍歷效率。
(2).遍歷時使用一個數組,此時可向另一個數組添加就緒的選擇鍵,防止ConcurrentModificationException異常發生。
再看其中的add()方法:

    @Override
    public boolean add(SelectionKey o) {
        if (o == null) {
            return false;   // 不支持null元素
        }

        if (isA) {
            int size = keysASize;
            keysA[size ++] = o; // 就緒的選擇鍵放在末尾
            keysASize = size;
            if (size == keysA.length) {
                doubleCapacityA();  // 雙倍擴充容量
            }
        } else {
            int size = keysBSize;
            keysB[size ++] = o;
            keysBSize = size;
            if (size == keysB.length) {
                doubleCapacityB();
            }
        }
        return true;
    }

從代碼中可以看出,兩個雙數組可以視為無限容量且不支持null元素。由于雙數組一個用于遍歷,一個用于添加新元素,我們看關鍵的兩個數組切換的方法,其實現也很簡單,代碼如下:

    SelectionKey[] flip() {
        if (isA) {
            isA = false;
            keysA[keysASize] = null;    // 最末尾元素顯示置為null
            keysBSize = 0;  // B數組清空,用于添加元素
            return keysA;   // A數組返回,用于遍歷
        } else {
            isA = true;
            keysB[keysBSize] = null;
            keysASize = 0;
            return keysB;
        }
    }

分析完對SelectedKeySet的優化,我們看在NioEventLoop中的使用:

    // 返回用于遍歷的數組
    processSelectedKeysOptimized(selectedKeys.flip());
    
    private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
        for (int i = 0;; i ++) {
            final SelectionKey k = selectedKeys[i];
            if (k == null) {
                break;  // 注意SelectedKeySet的實現置最末尾元素為null,故必能跳出
            }
            selectedKeys[i] = null; // 設置為null,幫助GC進行回收

            final Object a = k.attachment();
            if (a instanceof AbstractNioChannel) {
                processSelectedKey(k, (AbstractNioChannel) a);  // Netty框架處理
            } else {
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);    // 用戶自定義處理
            }

            if (needsToSelectAgain) {   // 有必要重新選擇
                for (;;) {
                    i++;
                    if (selectedKeys[i] == null) {
                        break;
                    }
                    // 將上一次遍歷集合中未處理元素置null,幫助GC回收,防止泄露
                    selectedKeys[i] = null; 
                }

                selectAgain();  // 未處理元素也將添加到數組中
                selectedKeys = this.selectedKeys.flip();    // 取出遍歷數組
                i = -1; // 遍歷數組索引設置為-1是因為之后將執行i++從而還是從0開始遍歷
            }
        }
    }

到了這里,我們已經分析完大部分NioEventLoop的工作原理和實現,但Netty的實現遠不止這些,比如全局任務執行器GlobalEventExecutor,默認執行器DefaultEventExecutor,以及其他的ThreadPerChannelEventLoop,LocalEventLoop等等,由于我們很懶,所以不再講述。我們休整一會,然后前往下一個目的地:Netty的優雅退出機制。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容