【Q-01】與 parentChannel 相綁定的線程是在執行什麼任務時建立的?請談一下你的認識。
【RA】與 parentChannel 相綁定的任務是在執行 channel 注冊到 selector 的任務時建立的。
【Q-02】Netty 中像端口綁定等都是以任務的形式出現的,但源碼中出現了任務封裝為新的
任務,任務套任務的情況,這是為什麼?請談一下你的認識。
【RA】Netty 中像端口綁定等都是以任務的形式出現的,但源碼中出現了任務封裝為新的任
務,任務套任務的情況,這些不同等級的任務,都會完成不同的功能,是責任鍊設計模式的
展現。
【Q-03】由 EventLoop 綁定線程來完成的任務有幾種類型?分别存放在哪裡?請談一下你的
認識。
【RA】由 EventLoop 綁定線程來完成的任務有三種類型:
定時任務:該類型任務會存放到定時任務隊列 scheduledTaskQueue 中。
普通任務:該類型任務會存放到任務隊列 taskQueue 中。我們在目前解析的 Netty 源碼
中見到的任務,都是這類任務。當然,定時任務最終也會從定時任務隊列中逐個取出,
然後放入到 taskQueue 中來執行。
收尾任務:該類型任務會存放到任務隊列 tailTasks 中。其定義方式與普通任務定義方式
相同,隻不過由于其主要用于完成一些收尾工作,是以被添加到了 tailTasks 隊列中了。
【Q-04】NioEventLoop 中有一個成員變量 wakenUp,其是一個原子布爾類型。這個變量的值
對于 selector 選擇源碼的閱讀很重要。它的值代表什麼意義?請談一下你的看法。
【RA】NioEventLoop 中有一個成員變量 wakenUp,其是一個原子布爾類型。其取值意義為:
true:表示目前 eventLoop 所綁定的線程處于非阻塞狀态,即喚醒狀态
false:表示目前 eventLoop 所綁定的線程即将被阻塞
【Q-05】在 selector 中有一個方法 wakeup(),其意義對于 selector 選擇源碼的閱讀很重要。
但這個方法的意義僅從其方法名上來了解,很容易産生誤解。那麼這個方法表示什麼意思呢?
請談一下你的看法。
【RA】該方法會使選擇操作立即結束,儲存選擇結果到 selector。而選擇操作的結束,會使
其調用者線程被喚醒
【Q-06】在 selector 中有一個帶參數的 select()方法,請談一下你對這個方法的認識。
【RA】在selector中有一個帶參數的select()方法,這個方法的功能是阻塞式選擇就緒channel。
即其調用者方法所線上程在調用了這個方法後會發生阻塞。而喚醒阻塞的條件有五個,哪個
先到哪個其作用:
有 channel 就緒被選擇
wakeup()方法被調用
目前線程的阻塞被打斷
給定的阻塞時間到了
當空輪詢次數較多時會引發 CPU 占用率飙升,為了保護系統,也會結束 select()方法,
喚醒阻塞
【Q-07】在 selector 中有一個 selectNow()方法,請談一下你對這個方法的認識。
【RA】在 selector 中有一個 selectNow()方法,這個方法的功能是非阻塞式選擇就緒 channel。
這個方法隻會對注冊的 channel 周遊一次,在周遊過程中如果發現就緒的 channel,則直接
傳回,沒有發現,則傳回 0。
【Q-08】ScheduledFutureTask 中有一個常量 START_TIME,這個常量的意義是什麼?請談一
下你的認識。
【RA】START_TIME 是 static final long 類型的常量,static final 類型常量是在目前類加載時完
成的初始化,即該常量是類級别的常量,對于所有定時任務來說,這個 START_TIME 常量值
都是相同的。是以該常量表示意義就是目前定時任務類的加載時間點,而所有定時任務要推
遲執行的時間都是基于這具時間點的時長。
第 4 次直播課
【Q-01】NioEventLoop 中的 run()方法用于進行就緒 channel 的選擇、處理就緒 channel 的 IO
及完成任務隊列中的任務。其中有一個變量 ioRatio 較為重要,其有什麼作用?請談一下你
的認識。
【RA】NioEventLoop 中的 run()方法中有一個變量 ioRatio,其表示處理就緒 channel 的 IO 操
作與處理任務隊列任務用時的占比。其值不大于 100,表示占 100 的比例。例如 IO 操作用
時 10 秒,ioRatio 的值為 20,那麼處理任務隊列任務用時為 10 秒 * (100 - 20)/20 = 10 秒
* 5 = 50 秒。
【Q-02】NioEventLoop 對 selectedKeys 進行了優化,其是如何優化的?為什麼這樣做?請談
一下你的認識。
【RA】Netty 對 SelectedKeys 的優化簡單來說就是,将原來的簡單的 Set 集合封裝為了一個
新的類,這個類中維護着一個數組,用于存放原來存放在 Set 集合中的元素。我們知道,在
數組長度不變的情況下,數組的執行效率要高于 Set、List 等集合。因為數組元素是順序存
儲的,而集合元素是鍊式存儲的。
【Q-03】NioEventLoop 在對就緒 channel 進行處理時出現了對 readyOps 為 0 的處理。readyOps
表示沒有就緒 channel,為什麼對就緒 channel 的進行中會出現 readyOps 為 0 的情況?請談
一下你的認識。
【RA】NioEventLoop 在對就緒 channel 進行處理時出現了對 readyOps 為 0 的處理。readyOps
為 0 的結果是執行非阻塞選擇 selectNow()時可能傳回的值。selectNow()結束表示選擇結束,
選擇結束就需要對選擇結果進行處理,是以在對就緒 channel 進行處理時出現了對 readyOps
為 0 的處理。
【Q-04】NioEventLoop 在對就緒 channel 進行處理時出現了對 readyOps 為 0 的處理。處理方
式是執行了一次 unsafe.read()。readyOps 表示沒有就緒 channel,既然沒有就緒,那麼其要
讀什麼?為什麼要這樣操作?請談一下你的認識。
【RA】NioEventLoop 在對就緒 channel 進行處理時出現了對 readyOps 為 0 的處理。處理方
式是執行了一次 unsafe.read()。readyOps 表示沒有就緒 channel,既然沒有就緒,這裡的讀
操作僅僅就是一次“空讀”,僅執行了一次讀操作流程。這樣做是為了解決NIO中的一個Bug: 開課吧 Reythor 雷課程面試題暨知識點總結
講師:Reythor 雷
6
由于長時間沒有就緒 channel 而導緻的高速輪詢,高速輪詢就可能導緻 CPU 使用率飙升進而
使系統崩潰。僅執行一次讀操作流程,就會使“高速輪詢”降速下來。
【Q-05】Netty 中的定時任務從定義到被執行,都經曆了些什麼?請談一下你的認識。
【RA】Netty 中的定時任務被定義好後,會直接被放入到定時任務隊列,然後在定時任務對
應的 channel 被選擇并完成響應的 IO 後,會将定時任務從定時任務隊列取出并放入到任務
隊列中,有 channel 綁定的 EventLoop 綁定的線程完成執行。
【Q-05】Netty 中的定時任務從定義到被執行,都經曆了些什麼?請談一下你的認識。
【RA】Netty 中的定時任務被定義好後,會直接被放入到定時任務隊列,然後在定時任務對
應的 channel 被選擇并完成響應的 IO 後,會将定時任務從定時任務隊列取出并放入到任務
隊列中,有 channel 綁定的 EventLoop 綁定的線程完成執行。
【Q-07】Netty 中在從定時任務隊列中查找開始執行時間小于指定時間的定時任務時,為什
麼僅從隊列中 peek()一個任務判斷,而不是周遊整個定時任務隊列?請談一下你的認識。
【RA】Netty 中在從定時任務隊列中查找開始執行時間小于指定時間的定時任務時,僅從隊
列中 peek()一個任務判斷,若滿足小于等于指定時間的條件,則傳回這個定時任務;不滿足
則直接傳回 null,而不是周遊整個定時任務隊列。因為定時任務隊列中的任務時按照任務開
始執行時間,由小到大排列好的。若 peek()出的第一個不能滿足條件,後面的任務更不會滿
足。是以不用對定時任務隊列進行周遊。
【Q-08】Netty 在執行任務時其所使用的時間會與對就緒 channel 的 IO 執行時間存在比例關
系。Netty 對于這個任務執行時間是精确控制的嗎?請談一下你的認識。
【RA】Netty 在執行任務時其所使用的時間會與對就緒 channel 的 IO 執行時間存在比例關系。
Netty 對于這個任務執行時間不是精确控制的。其會每執行 64 個任務判斷一次是否逾時。這
樣的話就可能在第 64 個任務執行完畢時剛好不逾時,而在執行第 65 個任務時逾時。但由于
沒有進行逾時判斷,是以後面 64 個都會在逾時的情況下執行完畢。此時在判斷逾時,發現
已經逾時,這是才會結束對任務隊列的執行。
之是以這樣設計,是因為在這個過程中的目前時間計算都是使用的 System.nanoTime(),
這個時間擷取比較消耗系統資源。為了提高效率,才寫死為每 64 個任務判斷一次逾時。
【Q-09】Netty 的收尾任務是在什麼時候執行的,是在所有任務隊列中的任務執行完畢後才
執行嗎?請談一下你的認識。
【RA】Netty 的收尾任務不是在所有任務隊列中的任務執行完畢後才執行的。Netty 對于任
務的執行會有一個逾時時間,當逾時時間過期時就會執行收尾任務隊列中的任務,此時可能
任務隊列中的任務還沒執行完畢。
【Q-10】Netty Server中端口綁定、parentChannel注冊到selector、childChannel注冊到selector,
這三個都是以任務的完成的。這三個任務執行的順序是怎樣的?這三個任務的差別于聯系是
什麼?請談一下你的認識。
【RA】Netty Server 中這三個任務的執行順序是:parentChannel 注冊到 selector、端口綁定、
childChannel 注冊到 selector。
在 Netty Server 啟動時調用了 parentChannel 注冊到 selector 任務。在完成這個任務時完
成了從 parentEventLoopGroup 中選擇出一個 EventLoop,并于 parentChannel 相綁定,完成了
注冊任務添加到任務隊列,完成了與 EventLoop 相綁定的線程的建立與啟動。
在 Netty Server 啟動時調用了端口綁定任務。其僅僅完成了綁定任務添加到任務隊列。
至于執行,就由一直沒有停止執行的、與 parentChannel 相綁定的 EventLoop 相綁定的線程
自動完成。
在 Netty Server 啟動後,Netty Server 的連接配接請求到達後,調用了 childChannel 注冊到
selector 任務。在完成這個任務時完成了從 childEventLoopGroup 中選擇出一個 EventLoop,
并于 childChannel 相綁定,完成了注冊任務添加到任務隊列,完成了與 EventLoop 相綁定的
線程的建立與啟動。
【Q-11】Netty 中如何添加定時任務?請談一下你的認識。
【RA】Netty 中要想添加定時任務,隻需擷取到 channel 的 eventLoop,然後調用 eventLoop
的 schedule()方法即可添加一個 Runnable 定時任務到定時任務隊列。
【Q-12】Netty Client 端 Channel 的建立過程與 Netty Server 端 parentChannel 的建立過程相同,
但其初始化過程略有不同。不同主要展現在哪裡?請談一下你的看法。
【RA】parentChannel 在初始化過程中需要将連接配接處理器注冊到 channel 的 pipeline 中。這個
連接配接處理器用于處理Client端的連接配接操作,為Client端在Server處生成其對應的childChannel,
并注冊到相應的 selector。但 Client 端的 channel 無需注冊連接配接處理器,因為它是連接配接的發出
者,而非接收者。
【Q-13】Netty Client 在連接配接指定的 Netty Server 位址之前,首先解析了這個指定的位址。請
簡單談一下你對這個解析過程的認識。
【RA】Netty Client 在連接配接指定的 Netty Server 位址之前,首先解析了這個指定的位址。這個
解析就是将指定的主機名解析為了 IP,這個解析過程是一個異步過程。
【Q-01】ChannelPipeline 是在什麼時候建立的?請從源碼角度簡要談一下你的認識。
【RA】ChannelPipeline 是在建立 Channel 時建立的。一個 Channel 對應一個 Pipeline。建立
Channel 時使用的是反射機制,調用了 NioServerSocketChannel 的無參構造器。而該構造器最
終調用了 AbstractChannel 的構造器。ChannelPipeline 就是在 AbstractChannel 的構造器中創
建的。
【Q-02】Netty 中的 ChannelPipeline 是一個比較重要的概念,ChannelPipeline 本質上是個什
麼?其又是怎麼添加節點的?請簡單談一下你的認識。
【RA】ChannelPipeline 是在建立 Channel 是建立的,其是 Channel 一個很重要的成員。其本
質上是一個雙向連結清單,預設具有頭、尾兩個節點。除了這兩個節點外,其還可以通過
channelPipeline 的 addLast()方法向其中添加處理器節點。每一個處理器最終都會被封裝為一
下 channelPipeline 上的節點。
【Q-03】ChannelPipeline 中的處理器的删除過程都做了哪些重要工作?請談一下你的認識。
【RA】處理器從 ChannelPipeline 中的删除過程主要做了如下幾項工作:
從 ChannelPipeline 中查找是否存在該處理器對應的節點。若存在,則進行删除。
由于其删除的是節點,是以,會首先從 ChannelPipeline 中找到該處理器節點,然後從 開課吧 Reythor 雷課程面試題暨知識點總結
講師:Reythor 雷
8
ChannelPipeline 的雙向連結清單中删除該節點。
最後觸發該處理器 handlerRemoved()方法的執行
【Q-04】在添加處理器到 ChannelPipeline 時可以為該處理器指定名稱,若沒有指定系統會
為其自動生成一個名稱。這個自動生成的名稱格式是怎樣的?請談一下你的認識。
【RA】在将處理器添加到 ChannelPipeline 中時若沒有指定名稱,系統會自動為其生成一個
名稱,該名稱為該處理器類的簡單類名後跟一個#,然後是一個數字。從 0 開始嘗試。若該
名稱在 ChannelPipeline 中存在,則數字加一,直到找到不重複的數字為止。
【Q-05】在 ChannelInitializer 類上為什麼需要添加@Sharable?請談一下你的認識。
【RA】@Sharable 注解添加到一個處理器類上表示該處理器是共享的,可以被多次添加到同
一個 ChannelPipeline 中,也可以被添加到多個 ChannelPipeline 中。
服務端啟動類中定義的 ChannelInitializer 執行個體是在 Server 啟動時建立的,然後每過來一
個 Client 連接配接,就會将這個 ChannelInitializer 執行個體添加到一個 childChannel 的 pipeline 中。即
一個 ChannelInitializer 處理器執行個體被添加到了連接配接到目前 Server 的所有用戶端對應的所有
childChannel 的 pipeline 中。這也就是為什麼需要在 ChannelInitializer 類上添加@Sharable 注
解的原因。
【Q-06】對于 ChannelInitializer 處理器執行個體的建立、删除,都與一個 initMap 有成員變量相
關。請談一下你對這個 initMap 的認識。
【RA】ChannelInitializer 處理器是一個共享處理器,為了減少記憶體的使用,在其中定義了一
個成員變量 initMap。它是一個 JUC 的 Set 集合,其中存放着所有由該 ChannelInitializer 處理
器執行個體建立出的節點執行個體。
需要清楚一點:每個處理器都會通過 new 被建立為為一個節點後才能被添加到 pipeline
中。ChannelInitializer 是一個共享處理器,但通過其 new 出來的節點是多例的,不是共享的。
為了友善管理,在 ChannelInitializer 中維護了一個 JUC 的 Set 集合,集合元素為該處理器創
建出的節點執行個體。這個 Set 集合就是這個 initMap。
【Q-07】簡述在 Server 端 bootstrap 中定義的 ChannelInitializer 處理器的建立、添加時機,
及添加到哪個 channel 的 pipeline 中了?
【RA】在 Server 端 bootstrap 中定義的 ChannelInitializer 處理器的建立、添加時機,及添加
位置如下:
建立時機:在 Server 啟動時被建立
添加位置:最終會被添加到 childChannel 的 pipeline 中。因為其是通過 bootstrap 中的
childHandler()完成的初始化
添加時機:每過來一個 Client 連接配接,就會将該處理器添加到一個 childChannel 的 pipeline
中,但添加的這個處理器執行個體,都是在 Server 啟動時建立的那一個
【Q-08】簡述在 Client 端 bootstrap 中定義的 ChannelInitializer 處理器的建立、添加時機,及
添加到哪個 channel 的 pipeline 中了?
【RA】在 Client 端 bootstrap 中定義的 ChannelInitializer 處理器的建立、添加時機,及添加位
置如下:
建立時機:在 Client 啟動時被建立
添加位置:被添加到 Channel 的 pipeline 中,Client 端沒有 parentChannel 與 childChannel 開課吧 Reythor 雷課程面試題暨知識點總結
講師:Reythor 雷
9
的區分
添加時機:在 Client 啟動時被添加
【Q-09】我們注意到,在 Netty 中,執行完畢 ChannelInitializer 處理器的 initChannel()方法後,
馬上就會将這個處理器删除。為什麼?請談一下你的認識。
【RA】ChannelInitializer 是一個比較特殊的處理器。其作用就是将其重寫的 initChannel()方法
執行完畢,然後該處理器的曆史使命也就完成了,此時就可以将其從 pipeline 中删除了。一
般 initChannel()方法中都是放的一些向 channelPipeline 中添加普通處理器的語句,或一些對
channel 進行初始化的語句
【Q-01】ChannelInboundHandler 中都包含了哪一類的方法?請談一下你的認識。
【RA】ChannelInboundHandler 中包含了像 channelRead()、channelRegistered()、channelActive()
等回調方法,即由其它事件所觸發的發法。
【Q-02】ChannelOutboundHandler 中都包含了哪一類的方法?請談一下你的認識。
【RA】ChannelOutboundHandler 中包含了像 bind()、connet()、close()等方法,這些方法一般
都是由 Outbound 處理器執行個體主動調用執行的,而最終是由 channel 的 unsafe 完成的。
【Q-03】簡述一下 ChanneHandler 接口。
【RA】ChanneHandler 接口是 ChannelInboundHandler 與 ChannelOutboundHandler 接口的父
接口,其包含兩個方法 handlerAdded()與 handlerRemoved()。也就是說,這兩個方法是所有
處理器都具有的方法。
【Q-04】ChanneHandlerContext 接口對于 ChannelPipeline 的了解很重要,請簡述一下
ChanneHandlerContext 接口。
【RA】ChanneHandlerContext 執行個體就是一個 ChannelPipeline 節點,是一個雙向連結清單節點,其
可以調用 InboundHandler 的方法,也可以調用 OutboundHandler 的方法,以引來觸發下一個
節點相應方法的執行。同時也可以擷取到設定到 channel 中的 attr 屬性。
【Q-05】Channe 中的 attr 屬性是在哪裡設定的?
【RA】無論是 Server 還是 Client,它們在啟動時會建立并初始化 bootstrap,此時可以調用其
attr()或 childAttr()方法,将指定的 attr 屬性初始化到 bootstrap 中。然後在 channel 建立後進
行初始化時會将 bootstrap 中配置的設定資訊初始化到 channel 中。這些資訊中就包含 attr
屬性。
【Q-06】簡述一下 ChannePipeline 接口及其重要實作類 DefaultChannelPipeline。
【RA】ChannePipeline 是一個 ChannelHandlers 清單。該接口是繼承了 ChannelInboundInvoker、
ChannelOutboundInvoker 接口,說明其可以觸發 Inbound 處理器方法,可以調用 Outbound
處理器方法。同時,其也繼承了 Iterator 操,說明其是可疊代的。
該接口有一個重要實作類 DefaultChannelPipeline。
DefaultChannelPipeline 類實作了 ChannelPipline 接口中有關 ChannelInboundInvoker 中的 開課吧 Reythor 雷課程面試題暨知識點總結
講師:Reythor 雷
10
方法,這些方法基本都是調用了抽象節點類 AbstractChannelHandlerContext 的相關靜态方法,
去調用 head 節點的相應方法。
DefaultChannelPipeline 類還實作了 ChannelPipline 接口中有關 ChannelOutboundInvoker
中的方法,這些方法基本都是調用 tail 結點的相關方法,完成底層的真正執行。
另外,DefaultChannelPipeline 類還實作了 ChannelPipline 接口中 Iterable 接口的方法
iterator()。其疊代的是一個 map 的 entrySet,這個 map 的 key 為節點名稱,而 value 為節點
所封裝的處理器執行個體。
【Q-07】簡述一下 ChannelInboundHandlerAdapter 與 SimpleChannelInboundHandler 處理器
的差別及應用場景。
【RA】若我們使用 ChannelInboundHandlerAdapter,則需要我們自己釋放 msg,而使用
SimpleChannelInboundHandler,則系統會自動釋放。是以,使用哪個類作為處理器的父類,
關鍵要看是否有需要釋放的消息。
一般情況下,若 channelRead()中從對端接收到的 msg(或其封裝執行個體)需要通過
writeAndFlush() 等 方 法 發 送 給 對 端 , 則 該 msg 不 能 釋 放 , 所 以 需 要 使 用
ChannelInboundHandlerAdapter 由我們自行控制 msg 的釋放。當然,若根本就不需要從對端
讀取資料,則直接使用 ChannelInboundHandlerAdapter。若使用 SimpleChannelInboundHandler
還需要重寫 channelRead0()方法。
【Q-08】ChannelPipline 與 ChannelHandlerContext 都具有 fireChannelRead()方法,請簡述一
下它們的差別。
【RA】ChannelPipline 中的 fireChannelRead()方法會從 head 節點的 channelRead()方法開始觸
發 pipeline 中節點的 channelRead()方法;而 ChannelHandlerContext 中的 fireChannelRead()方
法則是觸發目前節點的後面節點的 channelRead()方法。
【Q-09】簡述在 pipeline 中的多個處理器中都定義了 channelActive()與 handlerAdded()兩個方
法,請簡述它們執行的差別。
【RA】在 pipeline 中的多個處理器中的多個 channelActive()方法,隻有第一個該方法會執行,
因為 channel 隻會被激活一次。而 handlerAdded()方法則不同,是以處理器中的該方法都會
在目前處理器被添加到 pipeline 時被觸發執行。
【Q-10】簡述消息在 inboundHandler、outboundHandler 中的傳遞順序,及發生異常後,異
常資訊在 inboundHandler、outboundHandler 中的傳遞順序。
【RA】消息在 inboundHandler 中 channelRead()方法中的傳遞順序為,從 head 節點開始逐個
向後傳遞,直到傳遞給 tail 節點将該消息釋放。
消息在outboundHandler中write()方法中的傳遞順序為,從tail節點開始逐個向前傳遞,
直到傳遞到 head 節點,然後調用 unsafe 的 write()方法完成底層寫操作。
若發生異常,異常資訊會從目前發生異常的節點開始調用 exceptionCaught()方法,并向
後面節點傳遞,無論後面節點是 inboundHandler 還是 outboundHandler,最後傳遞到 tail 節
點的 exceptionCaught()方法,将異常消息釋放。
當然,前述的向後傳遞或向前傳遞的前提是,必須要在節點方法中調用傳遞到下一個節
點的方法,否則是無法傳遞的。