我們再次回顧這幅圖,通過先前的講解,現在是不是親切很多了。圖中綠色的acceptor應該是你最熟悉的部分,之前我們在ServerBootstrap中進行了詳細分析。我們知道了mainReactor是一個線程池,處理Accept事件負責接受客戶端的連接;subReactor也是一個線程池,處理Read(讀取客戶端通道上的數據)、Write(將數據寫入到客戶端通道上)等事件。在這一節中,我們將深入分析這兩個線程池的實現,不斷完善其中的細節。我們首先從類圖開始。
4.1 類圖
看到這幅類圖,如果你的第一印象是氣勢恢宏,那么恭喜你,你已經成功了一半。但不難預料的是,大多數人和我的感受是一樣的:這么多類,一定很累。好在這只是第一印象,我們仔細觀察,便會發現其中明顯的脈絡,兩條線索(這里使用自下而上):NioEventLoop以及NioEventLoopGroup即線程和線程池。忽略其中大量的接口,剩余這樣的兩條線:
NioEventLoop --> SingleThreadEventLoop --> SingleThreadEventExecutor -->
AbstractScheduledEventExecutor --> AbstractScheduledEventExecutor -->
AbstractEventExecutor --> AbstractExecutorService
NioEventLoopGroup --> MultithreadEventLoopGroup -->
MultithreadEventExecutorGroup --> AbstractEventExecutorGroup
下面我們正式開始分析,依舊使用自頂向下的方法,從類圖頂部向下、從線程池到線程分析。
4.2 EventExecutorGroup
EventExecutorGroup在類圖中處于承上啟下的位置,其上是Java原生的接口和類,其下是Netty新建的接口和類,由于它處于如此重要的位置,我們詳細分析其中的方法。
4.2.1 Executor
首先看其繼承自Executor的方法:
// Executes the given command at some time in the future
void execute(Runnable command);
只有一個簡單的execute()方法,但這個方法奠定了java并發的基礎,提供了異步執行任務的。
4.2.2 ExecutorService
ExecutorService的關鍵方法如下(其中的invoke***方法并非關鍵,不再列出):
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
這些方法我們能從命名中便能知道方法的作用。我們主要看submit()方法,該方法是execute()方法的擴展,相較于execute不關心執行結果,submit返回一個異步執行結果Future。這無疑是很大的進步,但這里的Future不提供回調操作,顯得很雞肋,所以Netty將Java原生的java.util.concurrent.Future擴展為io.netty.util.concurrent.Future,我們將在之后進行介紹。
4.2.3 ScheduledExecutorService
從名字可以看出,該接口提供了一系列調度方法:
ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
<V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,
long period,TimeUnit unit);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,
long delay,TimeUnit unit);
schedule()方法調度任務使任務在延遲一段時間后執行。scheduleAtFixedRate延遲一段時間后以固定頻率執行任務,scheduleWithFixedDelay延遲一段時間后以固定延時執行任務。是不是有點頭暈?那就對了,這里有一個例子專門治頭暈。專家建議程序員應該每小時工作50分鐘,休息10分鐘,類似這樣:
13:00 - 13:10 休息
13:10 - 14:00 寫代碼
14:00 - 14:10 休息
14:10 - 15:00 寫代碼
實現這樣的調度我們可以使用(假設現在時間為13:00):
executor.scheduleAtFixedRate(new RestRunnable(), 0 , 60, TimeUnit.MINUTES);
executor.scheduleWithFixedDelay(new RestRunnable(), 0 , 50, TimeUnit.MINUTES);
4.2.4 EventExecutorGroup
boolean isShuttingDown();
Future<?> shutdownGracefully();
Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit);
Future<?> terminationFuture();
EventExecutor next();
EventExecutorGroup擴展的方法有5個,前四個可以從命名中推斷出功能。shutdownGracefully()我們已經在Bootstrap一節中使用過,優雅關閉線程池;terminationFuture()返回線程池終止時的異步結果。重點關注next()方法,該方法的功能是從線程池中選擇一個線程。EventExecutorGroup還覆蓋了一些方法,我們不再列出,如果你感興趣可以去源碼里面查看,需要注意的是,覆蓋的方法大部分是將Java原生的java.util.concurrent.Future返回值覆蓋為io.netty.util.concurrent.Future。
4.3 線程池
4.3.1 AbstractEventExecutorGroup
AbstractEventExecutorGroup實現了EventExecutorGroup接口的大部分方法,實現都長的和下面的差不多:
@Override
public void execute(Runnable command) {
next().execute(command);
}
從這段代碼可以看出這個線程池和程序員有一個相同點:懶。當線程池執行一個任務或命令時,步驟是這樣的:(1).找一個線程。(2).交給線程執行。
4.3.2 MultithreadEventExecutorGroup
MultithreadEventExecutorGroup實現了線程的創建和線程的選擇,其中的字段為:
// 線程池,數組形式可知為固定線程池
private final EventExecutor[] children;
// 線程索引,用于線程選擇
private final AtomicInteger childIndex = new AtomicInteger();
// 終止的線程個數
private final AtomicInteger terminatedChildren = new AtomicInteger();
// 線程池終止時的異步結果
private final Promise<?> terminationFuture =
new DefaultPromise(GlobalEventExecutor.INSTANCE);
// 線程選擇器
private final EventExecutorChooser chooser;
MultithreadEventExecutorGroup的構造方法很長,我們將選出其中的關鍵部分分析,故不列出整體代碼。如果你是處女座,這里有一個鏈接MultithreadEventExecutorGroup。
我們先看構造方法簽名:
protected MultithreadEventExecutorGroup(int nThreads,
ThreadFactory threadFactory, Object... args)
其中的nThreads表示線程池的固定線程數。
MultithreadEventExecutorGroup初始化的步驟是:
(1).設置線程工廠
(2).設置線程選擇器
(3).實例化線程
(4).設置線程終止異步結果
首先我們看設備線程工廠的代碼:
if (threadFactory == null) {
threadFactory = newDefaultThreadFactory();
}
protected ThreadFactory newDefaultThreadFactory(),() {
return new DefaultThreadFactory(getClass());
}
如果構造參數threadFactory為空則使用默認線程池,創建默認線程池使用newDefaultThreadFactory(),這是一個protected方法,可以在子類中覆蓋實現。
接著我們看設置線程選擇器的代碼:
if (isPowerOfTwo(children.length)) {
chooser = new PowerOfTwoEventExecutorChooser();
} else {
chooser = new GenericEventExecutorChooser();
}
如果線程數是2的冪次方使用2的冪次方選擇器,否則使用通用選擇器。下次如果有面試官問你怎么判斷一個整數是2的冪次方,請甩給他這一行代碼:
private static boolean isPowerOfTwo(int val) {
return (val & -val) == val;
}
Netty實現了兩個線程選擇器,雖然代碼不一致,功能都是一樣的:每次選擇索引為上一次所選線程索引+1的線程。如果你沒看明白代碼的含義,沒關系,再看一遍。
private interface EventExecutorChooser {
EventExecutor next();
}
private final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
@Override
public EventExecutor next() {
return children[childIndex.getAndIncrement() & children.length - 1];
}
}
private final class GenericEventExecutorChooser implements EventExecutorChooser {
@Override
public EventExecutor next() {
return children[Math.abs(childIndex.getAndIncrement() % children.length)];
}
}
最佳實踐:線程池數量使用2的冪次方,這樣線程池選擇線程時使用位操作,能使性能最高。
下面我們接著分析實例化線程的步驟:
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
// 使用模板方法newChild實例化一個線程
children[i] = newChild(threadFactory, args);
success = true;
} catch (Exception e) {
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
if (!success) {
// 如果不成功,所有已經實例化的線程優雅關閉
for (int j = 0; j < i; j ++) {
children[j].shutdownGracefully();
}
// 確保已經實例化的線程終止
for (int j = 0; j < i; j ++) {
EventExecutor e = children[j];
try {
while (!e.isTerminated()) {
e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
break;
}
}
}
}
}
實現的過程一句話描述就是:使用newChild()依次實例化線程,如果出錯,關閉所有已經實例化的線程。也許你對finally中的代碼有疑問,這是因為不清楚shutdownGracefully()的含義。你需要提前明白這樣的事實:shutdownGracefully()只是通知線程池該關閉,但什么時候關閉由線程池決定,所以需要使用e.isTerminated()來判斷線程池是否真正關閉。
實例化線程池正常完成后,Netty使用下面的代碼設置異步終止結果:
final FutureListener<Object> terminationListener = new FutureListener<Object>() {
@Override
public void operationComplete(Future<Object> future) throws Exception {
// 線程池中的線程每終止一個增加記錄數,直到全部終止設置線程池異步終止結果為成功
if (terminatedChildren.incrementAndGet() == children.length) {
terminationFuture.setSuccess(null);
}
}
};
for (EventExecutor e: children) {
e.terminationFuture().addListener(terminationListener);
}
分析完MultithreadEventExecutorGroup的構造方法,我們繼續分析普通方法。它的普通方法基本與下面的isTerminated()類似:
@Override
public boolean isTerminated() {
for (EventExecutor l: children) {
if (!l.isTerminated()) {
return false;
}
}
return true;
}
總結起來就是:線程池的狀態由其中的各個線程決定。明白了這點,我們使用類比的方法可以推知其他方法的實現,故不再具體分析。
4.3.3 MultithreadEventLoopGroup
MultithreadEventLoopGroup實現了EventLoopGroup接口的方法,EventLoopGroup接口作為Netty并發的關鍵接口,我們看其中擴展的方法:
// 將通道channel注冊到EventLoopGroup中的一個線程上
ChannelFuture register(Channel channel);
// 返回的ChannelFuture為傳入的ChannelPromise
ChannelFuture register(Channel channel, ChannelPromise promise);
// 覆蓋父類接口的方法,返回EventLoop
@Override EventLoop next();
這些方法在MultithreadEventLoopGroup的具體實現很簡單。register()方法選擇一個線程,該線程負責具體的register()實現。next()方法使用父類實現,即使用上一節所述的選擇器選擇一個線程。代碼如下:
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
@Override
public EventLoop next() {
return (EventLoop) super.next();
}
分析完這些代碼,我們關注一下線程數的默認設置。
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads",
Runtime.getRuntime().availableProcessors() * 2));
默認情況,線程數最小為1,如果配置了系統參數io.netty.eventLoopThreads
,設置為該系統參數值,否則設置為核心數的2倍。
4.3.4 NioEventLoopGroup
NioEventLoopGroup的主要代碼實現是模板方法newChild(),用來創建線程池中的單個線程,代碼如下:
@Override
protected EventExecutor newChild(ThreadFactory threadFactory, Object... args)
throws Exception {
return new NioEventLoop(this, threadFactory, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(),
(RejectedExecutionHandler) args[2]);
}
關于代碼中的參數含義,我們放在NioEventLoop中分析。此外NioEventLoopGroup還提供了setIoRatio()和rebuildSelectors()兩個方法,一個用來設置I/O任務和非I/O任務的執行時間比,一個用來重建線程中的selector來規避JDK的epoll 100% CPU Bug。其實現也是依次設置各線程的狀態,故不再列出。