作者 | joel.wang老王
既然jdk已經有threadlocal,為何netty還要自己造個fastthreadlocal?fastthreadlocal快在哪裡?
這需要從jdk threadlocal的本身說起。如下圖:
在java線程中,每個線程都有一個threadlocalmap執行個體變量(如果不使用threadlocal,不會建立這個map,一個線程第一次通路某個threadlocal變量時,才會建立)。
該map是使用線性探測的方式解決hash沖突的問題,如果沒有找到空閑的slot,就不斷往後嘗試,直到找到一個空閑的位置,插入entry,這種方式在經常遇到hash沖突時,影響效率。
fastthreadlocal(下文簡稱ftl)直接使用數組避免了hash沖突的發生,具體做法是:每一個fastthreadlocal執行個體建立時,配置設定一個下标index;配置設定index使用atomicinteger實作,每個fastthreadlocal都能擷取到一個不重複的下标。
當調用<code>ftl.get()</code>方法擷取值時,直接從數組擷取傳回,如<code>return array[index]</code>,如下圖:
根據上文圖示可知,ftl的實作,涉及到internalthreadlocalmap、fastthreadlocalthread和fastthreadlocal幾個類,自底向上,我們先從internalthreadlocalmap開始分析。
internalthreadlocalmap類的繼承關系圖如下:
數組indexedvariables就是用來存儲ftl的value的,使用下标的方式直接通路。nextindex在ftl執行個體建立時用來給每個ftl執行個體配置設定一個下标,slowthreadlocalmap線上程不是ftlt時使用到。
internalthreadlocalmap的主要屬性:
比較簡單,<code>newindexedvariabletable()</code>方法建立長度為32的數組,然後初始化為unset,然後傳給父類。之後ftl的值就儲存到這個數組裡面。
注意,這裡儲存的直接是變量值,不是entry,這是和jdk threadlocal不同的。internalthreadlocalmap就先分析到這,其他方法在後面分析ftl再具體說。
要發揮ftl的性能優勢,必須和ftlt結合使用,否則就會退化到jdk的threadlocal。ftlt比較簡單,關鍵代碼如下:
ftlt的訣竅就在threadlocalmap屬性,它繼承java thread,然後聚合了自己的internalthreadlocalmap。後面通路ftl變量,對于ftlt線程,都直接從internalthreadlocalmap擷取變量值。
ftl實作分析基于netty-4.1.34版本,特别地聲明了版本,是因為在清除的地方,該版本的源碼已經注釋掉了objectcleaner的調用,和之前的版本有所不同。
非常簡單,就是給屬性index指派,指派的靜态方法在internalthreadlocalmap:
可見,每個ftl執行個體以步長為1的遞增序列,擷取index值,這保證了internalthreadlocalmap中數組的長度不會突增。
1.先來看看<code>internalthreadlocalmap.get()</code>方法如何擷取threadlocalmap:
因為結合fastthreadlocalthread使用才能發揮fastthreadlocal的性能優勢,是以主要看fastget方法。該方法直接從ftlt線程擷取threadlocalmap,還沒有則建立一個internalthreadlocalmap執行個體并設定進去,然後傳回。
2.<code>threadlocalmap.indexedvariable(index)</code>就簡單了,直接從數組擷取值,然後傳回:
3.如果擷取到的值不是unset,那麼是個有效的值,直接傳回。如果是unset,則初始化。
<code>initialize(threadlocalmap)</code>方法:
3.1.擷取ftl的初始值,然後儲存到ftl裡的數組,如果數組長度不夠則擴充數組長度,然後儲存,不展開。
3.2.<code>addtovariablestoremove(threadlocalmap, this)</code>的實作,是将ftl執行個體儲存在threadlocalmap内部數組第0個元素的set集合中。
此處不貼代碼,用圖示如下:
4.<code>registercleaner(threadlocalmap)</code>的實作,netty-4.1.34版本中的源碼:
由于objectcleaner.register這段代碼在該版本已經注釋掉,而餘下邏輯比較簡單,是以不再做分析。
随着<code>get()</code>方法分析完畢,<code>set(value)</code>方法原理也呼之欲出,限于篇幅,不再單獨分析。
前文說過,ftl要結合ftlt才能最大地發揮其性能,如果是其他的普通線程,就會退化到jdk的threadlocal的情況,因為普通線程沒有包含internalthreadlocalmap這樣的資料結構,接下來我們看如何退化。
從internalthreadlocalmap的<code>get()</code>方法看起:
從ftl看,退化操作的整個流程是:從一個jdk的threadlocal變量中擷取internalthreadlocalmap,然後再從internalthreadlocalmap擷取指定數組下标的值,對象關系示意圖:
在netty中對于ftl提供了三種回收機制:
自動: 使用ftlt執行一個被fastthreadlocalrunnable wrap的runnable任務,在任務執行完畢後會自動進行ftl的清理。
手動: ftl和internalthreadlocalmap都提供了remove方法,在合适的時候使用者可以(有的時候也是必須,例如普通線程的線程池使用ftl)手動進行調用,進行顯示删除。
自動: 為目前線程的每一個ftl注冊一個cleaner,當線程對象不強可達的時候,該cleaner線程會将目前線程的目前ftl進行回收。(netty推薦如果可以用其他兩種方式,就不要再用這種方式,因為需要另起線程,耗費資源,而且多線程就會造成一些資源競争,在netty-4.1.34版本中,已經注釋掉了調用objectcleaner的代碼。)
ftl在netty中最重要的使用,就是配置設定bytebuf。基本做法是:每個線程都配置設定一塊記憶體(poolarena),當需要配置設定bytebuf時,線程先從自己持有的poolarena配置設定,如果自己無法配置設定,再采用全局配置設定。
但是由于記憶體資源有限,是以還是會有多個線程持有同一塊poolarena的情況。不過這種方式已經最大限度地減輕了多線程的資源競争,提高程式效率。
具體的代碼在poolbytebufallocator的内部類poolthreadlocalcache中:
參考資料
netty源碼分析3 - fastthreadlocal 架構的設計
netty進階:自頂向下解析fastthreadlocal
往期推薦
spring boot實作定時任務的動态增删啟停
你在 docker 中跑 mysql?恭喜你,可以下崗了!
0.2秒居然複制了100g檔案?
spring boot中使用postgresql資料庫
聊聊前後端分離的接口規範