天天看點

吊打 ThreadLocal,談談FastThreadLocal為啥能這麼快?

吊打 ThreadLocal,談談FastThreadLocal為啥能這麼快?

作者 | joel.wang老王

既然jdk已經有threadlocal,為何netty還要自己造個fastthreadlocal?fastthreadlocal快在哪裡?

這需要從jdk threadlocal的本身說起。如下圖:

吊打 ThreadLocal,談談FastThreadLocal為啥能這麼快?

在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>​,如下圖:

吊打 ThreadLocal,談談FastThreadLocal為啥能這麼快?

根據上文圖示可知,ftl的實作,涉及到internalthreadlocalmap、fastthreadlocalthread和fastthreadlocal幾個類,自底向上,我們先從internalthreadlocalmap開始分析。

internalthreadlocalmap類的繼承關系圖如下:

吊打 ThreadLocal,談談FastThreadLocal為啥能這麼快?

數組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集合中。

此處不貼代碼,用圖示如下:

吊打 ThreadLocal,談談FastThreadLocal為啥能這麼快?

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擷取指定數組下标的值,對象關系示意圖:

吊打 ThreadLocal,談談FastThreadLocal為啥能這麼快?

在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資料庫​​

​​聊聊前後端分離的接口規範​​