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的優雅退出機制。