本文是我對Netty的NioEventLoopGroup及NioEventLoop初始化工作的原始碼閱讀筆記, 如下圖,是Netty的Reactor執行緒模型圖,本文描述NioEventLoopGroup等價於我在圖中標紅的MainReactor元件,全篇圍繞它的初始化展開,難免地方理解的不正確,歡迎留言
在Nio網路程式設計模型的圖示是下面那張圖, 單條Thread全職執行一個Selector,首先是服務端在啟動的時候,會把代表服務端的ServerSockerChannel註冊進Selector,且感興趣的事件是Accept, 一旦有客戶端請求建立連線,ServerSockerChannel的accept事件就會被Selector感知到,進行下一步處理
對NioEventLoopGroup最感性的認識,是在一定程度上,它其實是對上圖元件的封裝,那麼封裝了哪些部分呢?
- 對Thread的封裝
NioEventLoopGroup維護的是事件迴圈,EventLoop, 在Netty的主從Reactor執行緒模型中,兩個事件迴圈組其實也是執行緒組,因為每一個EventLoop在他的整個生命週期中都始終和一條執行緒唯一繫結,EventLoop的執行緒使用的是它自己封裝的FastThreadLocalThread
, 這條執行緒使得EventLoop有了處理事件的能力
- 對Selector的封裝
NioEventLoopGroup維護的是事件迴圈,EventLoop,同樣維護著屬於自己的Selector選擇器,這個選擇器使得EventLoop擁有了輪詢繫結在自己身上的Channel的能力. 並且Netty對JDK原生的選擇器做出了升級,使用自定義的陣列替換了原生Selector的HashSet集合SelectedKeys
,使得時間的複雜度在任何時刻都是O1
現在看,每一個EventLoop都是一個工作單元, 它的初始化過程是怎樣的? 下面就開始正式的閱讀原始碼
上圖是一個簡化了體系圖,,慢慢擼出他們的關係
這個圖使我們下面原始碼的流程圖, 完成NioEventLoopGroup
的初始化
入口:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
經過幾層this()
構造方法的呼叫,我們來到它的這個構造方法, 它呼叫了父類的構造方法,帶上了預設的select策略,以及拒絕執行任務的handler
public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,
final SelectStrategyFactory selectStrategyFactory) {
//todo 0 null 根據系統選出 nioXXXprovider 預設的選擇策略
// todo 呼叫父類的構造方法 MultithreadEventLoopGroup 多執行緒的事件迴圈組
super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
接著進入super()
, 進入MultiThreadEventLoopGroup
多執行緒事件迴圈組, 它幹了件大事, 初始化了NioEventLoopGroup執行緒的數量,注意,是數量, 原始碼在下面: 當然,我們一般會把BossGroup的執行緒數量設定為1
// todo 當MultithreadEventLoopGroup被載入進 JVM就會執行, 對 DEFAULT_EVENT_LOOP_THREADS進行初始化
static {
// todo max方法取最大值,
// todo SystemPropertyUtil.getInt,這是個系統輔助類, 如果系統中有 io.netty.eventLoopThreads,就取它, 沒有的話,去後面的值
// todo NettyRuntime.availableProcessors() 是當前的 系統的核數*2 , 在我的電腦上 2*2*2=8條執行緒
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
/**
* @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, Object...)
*/
// todo 接著 使用父類的構造方法, nThreads= DEFAULT_EVENT_LOOP_THREADS
// todo Object... args 是 selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()的簡寫
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
接著進入它的Supper()
來到了它的父類的構造方法,MultiThreadEventLoopExcutorGroup
的構造方法, 這個類是至關重要的類,它做了大量的初始化工作, 總體上看:
- 首先: 根據上一步確定的它可以擁有的執行緒數,迴圈建立並初始化一個
EventExecutor[]
這個陣列其實就是盛放EventLoop的陣列, 當這個for迴圈結束後,實際上NioEventLoopGroup
就新增完成了EventLoop
- 初始化選擇器工廠, 這個工廠的作用是, 當出現新的IO事件需要處理時,通過工廠的輪詢演算法,從
NioEventLoopGroup
中選取一個NioEventLoop
處理
原始碼:
// todo 在這個構造方法中,完成了一些屬性的賦值, 徹底構造完成 事件迴圈組物件
// todo Object... args 是 selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()的簡寫
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
}
if (executor == null) {
// todo 下面需要的引數,一開始使用無參的構造方法時, 傳遞進來的 就是null ,執行這一行程式碼, 建立預設的執行緒工廠
/// todo ThreadPerTaskExecutor 意味為當前的事件迴圈組 建立Executor , 用於 針對每一個任務的Executor 執行緒的執行器
// todo newDefaultThreadFactory根據它的特性,可以給執行緒加名字等,
// todo 比傳統的好處是 把建立執行緒和 定義執行緒需要做的任務分開, 我們只關心任務, 兩者解耦
// todo 每次執行任務都會建立一個執行緒實體
// todo NioEventLoop 執行緒命名規則 nioEventLoop-1-XX 1代表是第幾個group XX第幾個eventLoop
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
children = new EventExecutor[nThreads];
// todo 迴圈
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
// todo 建立EventLoop
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
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) {
// Let the caller handle the interruption.
Thread.currentThread().interrupt();
break;
}
}
}
}
}
// todo chooser 在這裡 初始化了
chooser = chooserFactory.newChooser(children);
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);
}
Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
}
這個過程中的細節:
- Netty的
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
預設的執行緒工廠,建立出的Thread,並不是JDK原生的Thread,而是Netty自己封裝的
protected Thread newThread(Runnable r, String name) {
System.out.println(threadGroup+" threadGroup");
return new FastThreadLocalThread(threadGroup, r, name);
}
- Netty的
ThreadPerTaskExecutor
原始碼如下, 可以看到,它的execute直接關聯著Thread.start()
方法, 一旦執行它就會開啟新的執行緒, 當然原始碼看到這裡時,它是沒有沒執行的,因為執行緒和NioEventLoop
關聯著,再往下就看NioEventLoop
的實現
* todo 這裡實際上使用了設計模式
* todo 1. command是使用者定義的任務, 命令模式; 直觀的 我定義一種任務, 程式不需要知道我執行的命令是什麼,但是當我把任務扔給你, 你幫我執行就好了
* todo 2. 代理設計模型, 代理了ThreadFactory , 把本來給ThreadPerTaskExecutor執行的任務給了ThreadFactory
*/
public final class ThreadPerTaskExecutor implements Executor {
private final ThreadFactory threadFactory;
public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
if (threadFactory == null) {
throw new NullPointerException("threadFactory");
}
this.threadFactory = threadFactory;
}
// todo 必須實現 Executor 裡面唯一的抽象方法, execute , 執行性 任務
@Override
public void execute(Runnable command) {
threadFactory.newThread(command).start();
}
}
其次,上面的newChild(executor, args);
方法其實是抽象方法,真正執行時會執行子類NioEventLoopGroup
的實現, 如下:
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
// todo !! 真正建立事件迴圈組的邏輯在這裡!!!
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}
// todo 這裡是 它的構造方法
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
// todo 進入到父類, 著重看他是如何建立出 TaskQueue的
super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
if (selectorProvider == null) {
throw new NullPointerException("selectorProvider");
}
if (strategy == null) {
throw new NullPointerException("selectStrategy");
}
// todo 常用的 屬性
provider = selectorProvider;
// todo 獲取Selector選擇器
final SelectorTuple selectorTuple = openSelector();
// todo SelectorTuple是netty維護 jdk 原生的Selector的包裝類, 下面看,他有兩個Selector, 一個是經過包裝的,一個是未經過包裝的
selector = selectorTuple.selector; //
unwrappedSelector = selectorTuple.unwrappedSelector; // todo Jdk 原生的Selector
selectStrategy = strategy;
}
繼續跟進去,上面的NioEventLoopGroup的體系圖也就分析到右半部分了,如上圖,是這半部分初始化工作的主要流程, 下面是它的構造方法,可以看到主要完成了兩件事
- 進入父類的構造方法完成任務佇列的建立
- 開啟選擇器,並且進行了優化
細節:
可以看到,現在已經進入了它的三級父類SingleThreadEventExecutor
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
boolean addTaskWakesUp, int maxPendingTasks,
RejectedExecutionHandler rejectedHandler) {
super(parent);
this.addTaskWakesUp = addTaskWakesUp;
this.maxPendingTasks = Math.max(16, maxPendingTasks);
// todo 儲存執行緒執行器
this.executor = ObjectUtil.checkNotNull(executor, "executor");
// todo 任務佇列 , 進入檢視
taskQueue = newTaskQueue(this.maxPendingTasks);
System.out.println(taskQueue.getClass());
rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}
在這個類中進行了如下的工作:
- 呼叫父類的構造方法,設定自己的父Group
- 初始化自己的執行緒執行器
- 建立任務佇列
佇列有啥用?
我們知道,Netty中的執行緒可不止一個, 多個EventLoop意味著多個執行緒, 任務佇列的作用就是當其他執行緒拿到CPU的執行權時,卻得到了其他執行緒的IO請求,這時當前執行緒就把這個請求以任務的方式提交到對應執行緒的任務佇列裡面
建立的什麼任務佇列?
有個誤區, 當我跟進newTaskQueue(this.maxPendingTasks);
方法時, 進入的方法建立了一個LinkedBlockingQueue佇列
, 實際上建立的確是MpscQueue
, 這並不奇怪,是因為NioEventLoop
把這個方法重寫了, 原始碼如下:
@Override
protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
// This event loop never calls takeTask()
return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
: PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
}
這個任務佇列的特性是 非阻塞的,多生產者單消費者, 正好和Netty的執行緒模型對應
此外,這個SingleThreadEventExecutor還有很多重要的方法
- excute執行任務
- 嘗試開啟執行緒(初始化EventLoop的執行緒)
- 開啟執行緒
- 執行所有任務
- 聚合定時任務
- 把任務丟進佇列
- 把任務從佇列中取出
NioEventLoop開啟自己的佇列時,做了哪些優化?
通過反射,藉助Java.Security.AccessController提供的執行緒安全的策略執行機制把原生JDK的selector的SelectedKeys這個HashSet替換成了陣列,使得他的事件複雜度在任何時刻都是O1
// todo 這裡進行了 優化, netty把hashSet轉換成了陣列, 因為在JDK的NIO模型中,獲取Selector時, Selector裡面內建的存放SelectionKey的容器是Set集合
// todo 而netty把它替換成了自己的資料結構, 陣列, 從而使在任何情況下, 它的時間複雜度都是 O1
private SelectorTuple openSelector() {
final Selector unwrappedSelector;
try {
// todo 使用jdk 的api建立新的 selector
unwrappedSelector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
}
if (DISABLE_KEYSET_OPTIMIZATION) { // todo 如果不需要優化,就返回原生的selector , 預設為false 即 使用優化
return new SelectorTuple(unwrappedSelector);
}
// todo 接下來 netty會用下面這個SelectedSelectionKeySet資料結構 替換原來的 keySet , 進入檢視
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
// todo 通過反射 sun.nio.ch.SelectorImpl 或者這個類
return Class.forName(
"sun.nio.ch.SelectorImpl",
false,
PlatformDependent.getSystemClassLoader());
} catch (Throwable cause) {
return cause;
}
}
});
// todo 判斷是否獲取到了這個類
if (!(maybeSelectorImplClass instanceof Class) ||
// ensure the current selector implementation is what we can instrument.
!((Class<?>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) {
if (maybeSelectorImplClass instanceof Throwable) {
Throwable t = (Throwable) maybeSelectorImplClass;
logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, t);
}
return new SelectorTuple(unwrappedSelector);
}
// todo 確定是Selector的實現類 換了個名字
final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
/**
* todo 類java.security.AccessController提供了一個預設的安全策略執行機制,它使用棧檢查來決定潛在不安全的操作是否被允許。
* todo 這個訪問控制器不能被例項化,它不是一個物件,而是集合在單個類中的多個靜態方法。
*/
Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
// todo 通過反射, 獲取到 selectorImplClass的兩個欄位 selectedKeys publicSelectedKeys
// todo selectedKeys publicSelectedKeys底層都是 hashSet() 實現的, 現在獲取出來了, 放入上面的陣列資料結構中
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
// todo trySetAccessible 可以強制訪問私有的物件
Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField);
if (cause != null) {
return cause;
}
// todo trySetAccessible 可以強制訪問私有的物件
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField);
if (cause != null) {
return cause;
}
// todo 真正的把通過反射得到的 那兩個欄位放入我們自己的資料結構中
// // todo 下面是把我們的NioEventLoop中的 unwrappedSelector 的 selectedKeysField的屬性 直接設定成 優化後的selectedKeySet
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
return null;
} catch (NoSuchFieldException e) {
return e;
} catch (IllegalAccessException e) {
return e;
}
}
});
if (maybeException instanceof Exception) {
selectedKeys = null;
Exception e = (Exception) maybeException;
logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e);
return new SelectorTuple(unwrappedSelector);
}
// todo 初始化自己維護被選中的key的集合 --> 陣列型別的
selectedKeys = selectedKeySet;
logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
return new SelectorTuple(unwrappedSelector,
new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
}
到現在為止, NioEventLoopGroup和NioEventLoop就都初始化完成了,當然這是初始化,程式執行到現在,依然只有一條主執行緒, EventLoop的Thrad還沒start()
幹活,但是起碼已經有能力準備啟動了
總結一下:
就像下面的體系一樣, 五臟俱全
- NioEventLoopGroup
- NIoEventLoop
- excutor(執行緒執行器) , 執行IO任務/非IO任務
- selector 選擇器
- Chooser
- NIoEventLoop
- excutor(執行緒執行器) , 執行IO任務/非IO任務
- selector 選擇器
- Chooser
- NIoEventLoop
- excutor(執行緒執行器) , 執行IO任務/非IO任務
- selector 選擇器
- Chooser
- NIoEventLoop