源碼之下無祕密 ── 作最好的 Netty 源碼分析教程git
Netty 源碼分析之 番外篇 Java NIO 的前生今世github
Java NIO 的前生今世 之一 簡介bootstrap
Java NIO 的前生今世 之二 NIO Channel 小結segmentfault
Netty 源碼分析之 一 揭開 Bootstrap 神祕的紅蓋頭多線程
此文章已同步發送到個人 github 上
這一章是 Netty 源碼分析 的第三章, 我將在這一章中你們一塊兒探究一下 Netty 的 EventLoop 的底層原理, 讓你們對 Netty 的線程模型有更加深刻的瞭解.
在 Netty 源碼分析之 一 揭開 Bootstrap 神祕的紅蓋頭 (客戶端) 章節中咱們已經知道了, 一個 Netty 程序啓動時, 至少要指定一個 EventLoopGroup(若是使用到的是 NIO, 那麼一般是 NioEventLoopGroup), 那麼這個 NioEventLoopGroup 在 Netty 中到底扮演着什麼角色呢? 咱們知道, Netty 是 Reactor 模型的一個實現, 那麼首先從 Reactor 的線程模型開始吧.
首先咱們來看一下 Reactor 的線程模型.
Reactor 的線程模型有三種:
單線程模型
多線程模型
主從多線程模型
首先來看一下 單線程模型:
所謂單線程, 即 acceptor 處理和 handler 處理都在一個線程中處理. 這個模型的壞處顯而易見: 當其中某個 handler 阻塞時, 會致使其餘全部的 client 的 handler 都得不到執行, 而且更嚴重的是, handler 的阻塞也會致使整個服務不能接收新的 client 請求(由於 acceptor 也被阻塞了). 由於有這麼多的缺陷, 所以單線程Reactor 模型用的比較少.
那麼什麼是 多線程模型 呢? Reactor 的多線程模型與單線程模型的區別就是 acceptor 是一個單獨的線程處理, 而且有一組特定的 NIO 線程來負責各個客戶端鏈接的 IO 操做. Reactor 多線程模型以下:
Reactor 多線程模型 有以下特色:
有專門一個線程, 即 Acceptor 線程用於監聽客戶端的TCP鏈接請求.
客戶端鏈接的 IO 操做都是由一個特定的 NIO 線程池負責. 每一個客戶端鏈接都與一個特定的 NIO 線程綁定, 所以在這個客戶端鏈接中的全部 IO 操做都是在同一個線程中完成的.
客戶端鏈接有不少, 可是 NIO 線程數是比較少的, 所以一個 NIO 線程能夠同時綁定到多個客戶端鏈接中.
接下來咱們再來看一下 Reactor 的主從多線程模型.
通常狀況下, Reactor 的多線程模式已經能夠很好的工做了, 可是咱們考慮一下以下狀況: 若是咱們的服務器須要同時處理大量的客戶端鏈接請求或咱們須要在客戶端鏈接時, 進行一些權限的檢查, 那麼單線程的 Acceptor 頗有可能就處理不過來, 形成了大量的客戶端不能鏈接到服務器.
Reactor 的主從多線程模型就是在這樣的狀況下提出來的, 它的特色是: 服務器端接收客戶端的鏈接請求再也不是一個線程, 而是由一個獨立的線程池組成. 它的線程模型以下:
能夠看到, Reactor 的主從多線程模型和 Reactor 多線程模型很相似, 只不過 Reactor 的主從多線程模型的 acceptor 使用了線程池來處理大量的客戶端請求.
咱們介紹了三種 Reactor 的線程模型, 那麼它們和 NioEventLoopGroup 又有什麼關係呢? 其實, 不一樣的設置 NioEventLoopGroup 的方式就對應了不一樣的 Reactor 的線程模型.
來看一下下面的例子:
EventLoopGroup bossGroup = new NioEventLoopGroup(1); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup) .channel(NioServerSocketChannel.class) ...
注意, 咱們實例化了一個 NioEventLoopGroup, 構造器參數是1, 表示 NioEventLoopGroup 的線程池大小是1. 而後接着咱們調用 b.group(bossGroup) 設置了服務器端的 EventLoopGroup. 有些朋友可能會有疑惑: 我記得在啓動服務器端的 Netty 程序時, 是須要設置 bossGroup 和 workerGroup 的, 爲何這裏就只有一個 bossGroup?
其實很簡單, ServerBootstrap 重寫了 group 方法:
@Override public ServerBootstrap group(EventLoopGroup group) { return group(group, group); }
所以當傳入一個 group 時, 那麼 bossGroup 和 workerGroup 就是同一個 NioEventLoopGroup 了.
這時候呢, 由於 bossGroup 和 workerGroup 就是同一個 NioEventLoopGroup, 而且這個 NioEventLoopGroup 只有一個線程, 這樣就會致使 Netty 中的 acceptor 和後續的全部客戶端鏈接的 IO 操做都是在一個線程中處理的. 那麼對應到 Reactor 的線程模型中, 咱們這樣設置 NioEventLoopGroup 時, 就至關於 Reactor 單線程模型.
同理, 再來看一下下面的例子:
EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) ...
bossGroup 中只有一個線程, 而 workerGroup 中的線程是 CPU 核心數乘以2, 所以對應的到 Reactor 線程模型中, 咱們知道, 這樣設置的 NioEventLoopGroup 其實就是 Reactor 多線程模型.
相信讀者朋友都想到了, 實現主從線程模型的例子以下:
EventLoopGroup bossGroup = new NioEventLoopGroup(4); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) ...
bossGroup 線程池中的線程數咱們設置爲4, 而 workerGroup 中的線程是 CPU 核心數乘以2, 所以對應的到 Reactor 線程模型中, 咱們知道, 這樣設置的 NioEventLoopGroup 其實就是 Reactor 主從多線程模型.
根據 @labmem 的提示, Netty 的服務器端的 acceptor 階段, 沒有使用到多線程, 所以上面的 主從多線程模型
在 Netty 的服務器端是不存在的.
服務器端的 ServerSocketChannel 只綁定到了 bossGroup 中的一個線程, 所以在調用 Java NIO 的 Selector.select 處理客戶端的鏈接請求時, 其實是在一個線程中的, 因此對只有一個服務的應用來講, bossGroup 設置多個線程是沒有什麼做用的, 反而還會形成資源浪費.
經 Google, Netty 中的 bossGroup 爲何使用線程池的緣由你們衆所紛紜, 不過我在 stackoverflow 上找到一個比較靠譜的答案:
the creator of Netty says multiple boss threads are useful if we share NioEventLoopGroup between different server bootstraps, but I don't see the reason for it.
所以上面的 主從多線程模型
分析是有問題, 抱歉.
在前面 Netty 源碼分析之 一 揭開 Bootstrap 神祕的紅蓋頭 (客戶端) 章節中, 咱們已經簡單地介紹了一下 NioEventLoopGroup 的初始化過程, 這裏再回顧一下:
即:
EventLoopGroup(實際上是MultithreadEventExecutorGroup) 內部維護一個類型爲 EventExecutor children 數組, 其大小是 nThreads, 這樣就構成了一個線程池
若是咱們在實例化 NioEventLoopGroup 時, 若是指定線程池大小, 則 nThreads 就是指定的值, 反之是處理器核心數 * 2
MultithreadEventExecutorGroup 中會調用 newChild 抽象方法來初始化 children 數組
抽象方法 newChild 是在 NioEventLoopGroup 中實現的, 它返回一個 NioEventLoop 實例.
NioEventLoop 屬性:
SelectorProvider provider 屬性: NioEventLoopGroup 構造器中經過 SelectorProvider.provider() 獲取一個 SelectorProvider
Selector selector 屬性: NioEventLoop 構造器中經過調用經過 selector = provider.openSelector() 獲取一個 selector 對象.
NioEventLoop 繼承於 SingleThreadEventLoop, 而 SingleThreadEventLoop 又繼承於 SingleThreadEventExecutor. SingleThreadEventExecutor 是 Netty 中對本地線程的抽象, 它內部有一個 Thread thread 屬性, 存儲了一個本地 Java 線程. 所以咱們能夠認爲, 一個 NioEventLoop 其實和一個特定的線程綁定, 而且在其生命週期內, 綁定的線程都不會再改變.
NioEventLoop 的類層次結構圖仍是比較複雜的, 不過咱們只須要關注幾個重要的點便可. 首先 NioEventLoop 的繼承鏈以下:
NioEventLoop -> SingleThreadEventLoop -> SingleThreadEventExecutor -> AbstractScheduledEventExecutor
在 AbstractScheduledEventExecutor 中, Netty 實現了 NioEventLoop 的 schedule 功能, 即咱們能夠經過調用一個 NioEventLoop 實例的 schedule 方法來運行一些定時任務. 而在 SingleThreadEventLoop 中, 又實現了任務隊列的功能, 經過它, 咱們能夠調用一個 NioEventLoop 實例的 execute 方法來向任務隊列中添加一個 task, 並由 NioEventLoop 進行調度執行.
一般來講, NioEventLoop 肩負着兩種任務, 第一個是做爲 IO 線程, 執行與 Channel 相關的 IO 操做, 包括 調用 select 等待就緒的 IO 事件、讀寫數據與數據的處理等; 而第二個任務是做爲任務隊列, 執行 taskQueue 中的任務, 例如用戶調用 eventLoop.schedule 提交的定時任務也是這個線程執行的.
從上圖能夠看到, SingleThreadEventExecutor 有一個名爲 thread 的 Thread 類型字段, 這個字段就表明了與 SingleThreadEventExecutor 關聯的本地線程.
下面是這個構造器的代碼:
protected SingleThreadEventExecutor( EventExecutorGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) { this.parent = parent; this.addTaskWakesUp = addTaskWakesUp; thread = threadFactory.newThread(new Runnable() { @Override public void run() { boolean success = false; updateLastExecutionTime(); try { SingleThreadEventExecutor.this.run(); success = true; } catch (Throwable t) { logger.warn("Unexpected exception from an event executor: ", t); } finally { // 省略清理代碼 ... } } }); threadProperties = new DefaultThreadProperties(thread); taskQueue = newTaskQueue(); }
在 SingleThreadEventExecutor 構造器中, 經過 threadFactory.newThread 建立了一個新的 Java 線程. 在這個線程中所作的事情主要就是調用 SingleThreadEventExecutor.this.run() 方法, 而由於 NioEventLoop 實現了這個方法, 所以根據多態性, 其實調用的是 NioEventLoop.run() 方法.
Netty 中, 每一個 Channel 都有且僅有一個 EventLoop 與之關聯, 它們的關聯過程以下:
從上圖中咱們能夠看到, 當調用了 AbstractChannel#AbstractUnsafe.register 後, 就完成了 Channel 和 EventLoop 的關聯. register 實現以下:
@Override public final void register(EventLoop eventLoop, final ChannelPromise promise) { // 刪除條件檢查. ... AbstractChannel.this.eventLoop = eventLoop; if (eventLoop.inEventLoop()) { register0(promise); } else { try { eventLoop.execute(new OneTimeTask() { @Override public void run() { register0(promise); } }); } catch (Throwable t) { ... } } }
在 AbstractChannel#AbstractUnsafe.register 中, 會將一個 EventLoop 賦值給 AbstractChannel 內部的 eventLoop 字段, 到這裏就完成了 EventLoop 與 Channel 的關聯過程.
在前面咱們已經知道了, NioEventLoop 自己就是一個 SingleThreadEventExecutor, 所以 NioEventLoop 的啓動, 其實就是 NioEventLoop 所綁定的本地 Java 線程的啓動.
依照這個思想, 咱們只要找到在哪裏調用了 SingleThreadEventExecutor 的 thread 字段的 start() 方法就能夠知道是在哪裏啓動的這個線程了.
從代碼中搜索, thread.start() 被封裝到 SingleThreadEventExecutor.startThread() 方法中了:
private void startThread() { if (STATE_UPDATER.get(this) == ST_NOT_STARTED) { if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { thread.start(); } } }
STATE_UPDATER 是 SingleThreadEventExecutor 內部維護的一個屬性, 它的做用是標識當前的 thread 的狀態. 在初始的時候, STATE_UPDATER == ST_NOT_STARTED
, 所以第一次調用 startThread() 方法時, 就會進入到 if 語句內, 進而調用到 thread.start().
而這個關鍵的 startThread() 方法又是在哪裏調用的呢? 通過方法調用關係搜索, 咱們發現, startThread 是在 SingleThreadEventExecutor.execute 方法中調用的:
@Override public void execute(Runnable task) { if (task == null) { throw new NullPointerException("task"); } boolean inEventLoop = inEventLoop(); if (inEventLoop) { addTask(task); } else { startThread(); // 調用 startThread 方法, 啓動EventLoop 線程. addTask(task); if (isShutdown() && removeTask(task)) { reject(); } } if (!addTaskWakesUp && wakesUpForTask(task)) { wakeup(inEventLoop); } }
既然如此, 那如今咱們的工做就變爲了尋找 在哪裏第一次調用了 SingleThreadEventExecutor.execute() 方法.
若是留心的讀者可能已經注意到了, 咱們在 EventLoop 與 Channel 的關聯 這一小節時, 有提到到在註冊 channel 的過程當中, 會在 AbstractChannel#AbstractUnsafe.register 中調用 eventLoop.execute 方法, 在 EventLoop 中進行 Channel 註冊代碼的執行, AbstractChannel#AbstractUnsafe.register 部分代碼以下:
if (eventLoop.inEventLoop()) { register0(promise); } else { try { eventLoop.execute(new OneTimeTask() { @Override public void run() { register0(promise); } }); } catch (Throwable t) { ... } }
很顯然, 一路從 Bootstrap.bind 方法跟蹤到 AbstractChannel#AbstractUnsafe.register 方法, 整個代碼都是在主線程中運行的, 所以上面的 eventLoop.inEventLoop() 就爲 false, 因而進入到 else 分支, 在這個分支中調用了 eventLoop.execute. eventLoop 是一個 NioEventLoop 的實例, 而 NioEventLoop 沒有實現 execute 方法, 所以調用的是 SingleThreadEventExecutor.execute:
@Override public void execute(Runnable task) { ... boolean inEventLoop = inEventLoop(); if (inEventLoop) { addTask(task); } else { startThread(); addTask(task); if (isShutdown() && removeTask(task)) { reject(); } } if (!addTaskWakesUp && wakesUpForTask(task)) { wakeup(inEventLoop); } }
咱們已經分析過了, inEventLoop == false, 所以執行到 else 分支, 在這裏就調用了 startThread() 方法來啓動 SingleThreadEventExecutor 內部關聯的 Java 本地線程了.
總結一句話, 當 EventLoop.execute 第一次被調用時, 就會觸發 startThread() 的調用, 進而致使了 EventLoop 所對應的 Java 線程的啓動.
咱們將 EventLoop 與 Channel 的關聯 小節中的時序圖補全後, 就獲得了 EventLoop 啓動過程的時序圖:
下一小節: Netty 源碼分析之 三 我就是大名鼎鼎的 EventLoop(二)
本文由 yongshun 發表於我的博客, 採用 署名-相同方式共享 3.0 中國大陸許可協議.
Email: yongshun1228@gmail .com
本文標題爲: Netty 源碼分析之 三 我就是大名鼎鼎的 EventLoop(一)
本文連接爲: http://www.javashuo.com/article/p-zbirqpbl-ga.html