這玩意比ThreadLocal叼多了,嚇得why哥趕忙分享出來。

這是why哥的第 70 篇原創文章java

從Dubbo的一次提交開始

故事得從前段時間翻閱 Dubbo 源碼時,看到的一段代碼講起。git

這段代碼就是這個:github

org.apache.dubbo.rpc.RpcContext面試

使用 InternalThreadLocal 提高性能。apache

相信做爲一個程序猿,都會被 improve performance(提高性能)這樣的字眼抓住眼球。segmentfault

內心開始癢癢的,必需要一探究竟。數組

剛看到這段代碼的時候,我就想:既然他是要提高性能,那說明以前的東西表現的不太好。安全

那以前的東西是什麼?數據結構

通過長時間的推理、縝密的分析,我大膽的猜想到以前的東西就是:ThreadLocal。mybatis

來,帶你們看一下:

果不其然,我真是太厲害了。

2018 年 5 月 15 日的提交:New threadLocal provides more performance. (#1745)

能夠看到此次提交的後面跟了一個數字:1745。它對應一個 pr,連接以下:

https://github.com/apache/dubbo/pull/1745

在這個 pr 裏面仍是有不少有趣的東西的,出場人物一個比一個騷,文章的最後帶你們看看。

能幹啥用?

在說 ThreadLocal 和 InternalThreadLocal 以前,仍是先講講它們是幹啥用的吧。

InternalThreadLocal 是 ThreadLocal 的加強版,因此他們的用途都是同樣的,一言蔽之就是:傳遞信息。

你想象你有一個場景,調用鏈路很是的長。當你在其中某個環節中查詢到了一個數據後,最後的一個節點須要使用一下。

這個時候你怎麼辦?你是在每一個接口的入參中都加上這個參數,傳遞進去,而後只有最後一個節點用嗎?

能夠實現,可是不太優雅。

你再想一想一個場景,你有一個和業務沒有一毛錢關係的參數,好比 traceId ,純粹是爲了作日誌追蹤用。

你加一個和業務無關的參數一路透傳幹啥玩意?

一般咱們的作法是放在 ThreadLocal 裏面,做爲一個全局參數,在當前線程中的任何一個地方均可以直接讀取。固然,若是你有修改需求也是能夠的,視需求而定。

絕大部分的狀況下,ThreadLocal 是適用於讀多寫少的場景中。

舉三個框架源碼中的例子,你們品一品。

第一個例子:Spring 的事務。

在個人早期做品《事務沒回滾?來,咱們從現象到原理一塊兒分析一波》裏面,我曾經寫過:

Spring 的事務是基於 AOP 實現的,AOP 是基於動態代理實現的。因此 @Transactional 註解若是想要生效,那麼其調用方,須要是被 Spring 動態代理後的類。

所以若是在同一個類裏面,使用 this 調用被 @Transactional 註解修飾的方法時,是不會生效的。

爲何?

由於 this 對象是未經動態代理後的對象。

那麼咱們怎麼獲取動態代理後的對象呢?

其中的一個方法就是經過 AopContext 來獲取。

其中第三步是這樣獲取的:AopContext.currentProxy();

而後我還很是高冷的(咦,想一想就以爲羞恥)說了句:對於 AopContext 我多說幾句。

看一下 AopContext 裏面的 ThreadLocal:

調用 currentProxy 方法時,就是從 ThreadLocal 裏面獲取當前類的代理類。

那他是怎麼放進去的呢?

我高冷的第二句是這樣說的:

對應的代碼位置以下:

能夠看到,通過一個 if 判斷,若是爲 true ,則調用 AopContext.setCurrentProxy 方法,把代理對象放到 AopContext 裏面去。

而這個 if 判斷的配置默認是 false,因此須要經過剛剛說的配置修改成 true,這樣 AopContext 纔會生效。

附送一個知識點給你,不客氣。

第二個例子:mybatis 的分頁插件,PageHelper。

使用方法很是簡單,從官網上截個圖:

這裏它爲何說:緊跟着的第一個 select 方法會被分頁。

或者說:什麼狀況下會致使不安全的分頁?

來,就當是一個面試題,而且我給你提示了:從 ThreadLocal 的角度去回答。

其實就是由於 PageHelper 方法使用了靜態的 ThreadLocal 參數,分頁參數和線程是綁定的:

若是咱們寫出下面這樣的代碼,就是不安全的用法:

這種狀況下因爲 param1 存在 null 的狀況,就會致使 PageHelper 生產了一個分頁參數,可是沒有被消費,這個參數就會一直保留在這個線程上,也就是放在線程的 ThreadLocal 裏面。

當這個線程再次被使用時,就可能致使不應分頁的方法去消費這個分頁參數,這就產生了莫名其妙的分頁。

上面這個代碼,應該寫成下面這個樣子:

這種寫法,就能保證安全。

核心思想就一句話:只要你能夠保證在 PageHelper 方法調用後緊跟 MyBatis 查詢方法,這就是安全的。

由於 PageHelper 在 finally 代碼段中自動清除了 ThreadLocal 存儲的對象。

就算代碼在進入 Executor 前發生異常,致使線程不可用的狀況,好比常見的接口方法名稱和 XML 中的不匹配,致使找不到 MappedStatement ,因爲自動清除,也不會致使 ThreadLocal 參數被錯誤的使用。

因此,我看有的人爲了保險起見這樣去寫:

怎麼說呢,這個代碼....

第三個例子:Dubbo 的 RpcContext。

RpcContext 這個對象裏面維護了兩個 InternalThreadLocal,分別是存放 local 和 server 的上下文。

也就是咱們說的加強版的 ThreadLocal:

做爲一個 Dubbo 應用,它既多是發起請求的消費者,也多是接收請求的提供者。

每一次發起或者收到 RPC 調用的時候,上下文信息都會發生變化。

好比說:A 調用 B,B 調用 C。這個時候 B 既是消費者也是提供者。

那麼當 A 調用 B,B 仍是沒調用 C 以前,RpcContext 裏面保存的是 A 調用 B 的上下文信息。

當 B 開始調用 C 了,說明 A 到 B 以前的調用已經完成了,那麼以前的上下文信息就應該清除掉。

這時 RpcContext 裏面保存的應該是 B 調用 C 的上下文信息。不然會出現上下文污染的狀況。

而這個上下文信息裏面的一部分就是經過InternalThreadLocal存放和傳遞的,是 ContextFilter 這個攔截器維護的。

ThreadLocal 在 Dubbo 裏面的一個應用就是這樣。

固然,還有不少不少其餘的開源框架都使用了 ThreadLocal 。

能夠說使用頻率很是的高。

什麼?你說你用的少?

那可不咋的,人家都給你封裝好了,你當個黑盒,開箱即用。

其實你用了,只是你不知道而已。

強在哪裏?

前面說了 ThreadLocal的幾個應用場景,那麼這個 InternalThreadLocal 到底比 ThreadLocal 強在什麼地方呢?

先說結論。

答案其實就寫在類的 javadoc 上:

InternalThreadLocal 是 ThreadLocal 的一個變種,當配合 InternalThread 使用時,具備比普通 Thread 更高的訪問性能。

InternalThread 的內部使用的是數組,經過下標定位,很是的快。若是遇得擴容,直接數組擴大一倍,完事。

而 ThreadLocal 的內部使用的是 hashCode 去獲取值,多了一步計算的過程,並且用 hashCode 必然會遇到 hash 衝突的場景,ThreadLocal 還得去解決 hash 衝突,若是遇到擴容,擴容以後還得 rehash ,這可不得慢嗎?

數據結構都不同了,這其實就是這兩個類的本質區別,也是 InternalThread 的性能在 Dubbo 的這個場景中比 ThreadLocal 好的根本緣由。

而 InternalThread 這個設計思想是從 Netty 的 FastThreadLocal 中學來的。

本文主要聊聊 InternalThread ,可是我但願的是你們能學到這個類的思想,而不是用法。

首先,咱們先搞個測試類:

public class InternalThreadLocalTest {

    private static InternalThreadLocal<Integer> internalThreadLocal_0 = new InternalThreadLocal<>();

    public static void main(String[] args) {
        new InternalThread(() -> {
            for (int i = 0; i < 5; i++) {
                internalThreadLocal_0.set(i);
                Integer value = internalThreadLocal_0.get();
                System.out.println(Thread.currentThread().getName()+":"+value);
            }
        }, "internalThread_have_set").start();

        new InternalThread(() -> {
            for (int i = 0; i < 5; i++) {
                Integer value = internalThreadLocal_0.get();
                System.out.println(Thread.currentThread().getName()+":"+value);
            }
        }, "internalThread_no_set").start();
    }
}

上面代碼的運行結果是這樣的:

因爲 internalThread_no_set 這個線程沒有調用 InternalThreadLocal 類的 set 方法,因此調用 get 方法輸出爲 null。

裏面主要用到了 set、get 這一對方法。

下面藉助 set 方法,帶你們看看內部原理(先說一下,爲了方便截圖,我有可能會調整一下源碼順序):

首先是判斷了傳進來的 value 是不是 null 或者是 UNSET,若是是則調用 remove 方法。

null 是好理解的。這個 UNSET 是個什麼鬼?

根據 UNSET 能很容易的找到這個地方:

原來是 InternalThreadLocalMap 初始化的時候會填充 UNSET 對象。

因此,若是 set 的對象是 UNSET,咱們能夠認爲是須要把當前位置上的值替換爲 UNSET,也就是 remove 掉。

並且,咱們還看到了兩個關鍵的信息:

1.InternalThreadLocalMap 雖然名字叫作 Map ,可是它掛羊頭賣狗肉,其實裏面維護的是一個數組。

2.數組初始化大小是 32。

接着咱們回去看 else 分支的邏輯:

調用的是 InternalThreadLocalMap 對象的 get 方法。

而這個方法裏面的兩個 get 就有趣了。

能走到 fastGet 方法的,說明當前線程是 InternalThread 類,直接能夠獲取到類裏面的 InternalThreadLocalMap。

若是走到 slowGet 了,則回退到原生的 ThreadLocal ,只是在原生的裏面,我仍是放的 InternalThreadLocalMap:

因此,其實線程上綁定的數據都是放到 InternalThreadLocalMap 裏面的,無論你操做什麼 ThreadLocal,實際上都是操做的 InternalThreadLocalMap。

那問題來了,你以爲一個叫作 fastGet ,一個叫作 slowGet。這個快慢,指的是 get 什麼東西的快慢?

對咯,就是獲取 InternalThreadLocalMap。

InternalThreadLocalMap 在 InternalThread 裏面是一個變量維護的,能夠直接經過 InternalThread.threadLocalMap() 得到:

標號爲 ① 的地方是獲取,標號爲 ② 的地方是設置。

都是一步到位,操做起來很是的方便。

這是 fastGet。

而 slowGet 是從 ThreadLocal 中獲取:

這裏的 get ,就是原生 ThreadLocal 的 get 方法,一眼望去,就複雜多了:

標號爲 ① 的地方,首先計算 hash 值,而後拿着 hash 值去數組裏面取數據。若是取出來的數據不是咱們想要的數據,則到標號爲 ② 的邏輯裏面去。

那麼我問你,除了這個位置上的值真的爲 null 外,還有什麼緣由會致使我拿着計算出來的 hash 值去數組裏面取數據取不到?

就是看你熟不熟悉 ThreadLocal 對 hash 衝突的處理方式了。

那麼這個問題稍微的升級一下就是:你知道哪些 hash 衝突的解決方案呢?

1.開放定址法。

2.鏈式地址法。

3.再哈希法。

4.創建公共溢出區。

咱們很是熟悉的 HashMap 就是採用的鏈式地址法解決 hash 衝突。

而 ThreadLocal 用的就是開放定址法中的線性探測。

所謂線性探測就是,若是某個位置的值已經存在了,那麼就在原來的值上日後加一個單位,直至不發生哈希衝突,就像這樣的:

上面的動圖就是須要在一個長度爲 7 的數組裏面,再放一個進過 hash 計算後爲下標爲 2 的數據,可是該位置上有值,也就是發生了 hash 衝突。

因而解決 hash 衝突的方法就是一次次的日後移,直到找到沒有衝突的位置。

因此,當咱們取值的時候若是發生了 hash 衝突也須要日後查詢,這就是上面標號爲 ③ 的 while 循環代碼的其中一個目的。

固然還有其餘目的,就隱藏在 440 行的 expungeStaleEntry 方法裏面。不是本文重點,就很少說了。

可是若是你不知道這個方法,你必定要去查閱一下相關的資料,有可能會在必定程度上改變你印象中的:用 ThreadLocal 會致使內存泄漏的風險。

至少,你能夠知道 JDK 爲了不內存泄漏的問題,是作了本身的最大努力的。

好了,不扯遠了,說回來。

從上面咱們知道了,從 ThreadLocal 中獲取 InternalThreadLocalMap 會經歷以下步驟:

1.計算 hash 值。

2.判斷經過 hash 值是否能直接獲取到目標對象。

3.若是沒有獲取到目標對象則日後遍歷,直至獲取成功或者循環結束。

比從 InternalThread 裏面獲取 InternalThreadLocalMap 複雜多了。

如今你知道了 fastGet/slowGet 這個兩個方法中的快慢,指的是從兩個不一樣的 ThreadLocal 中獲取 InternalThreadLocalMap 的操做的快慢。而快慢的根本緣由是數據結構的差別。

好,如今咱們獲取到 InternalThreadLocalMap 了,接着看 set 方法:

標號爲 ① 的地方就是往 InternalThreadLocalMap 這個數組中存放咱們傳進來的 value。

存的時候分爲兩種狀況。

標號爲 ② 的地方是數組容量還夠,能放進去,那麼能夠直接設置。

標號爲 ③ 的地方是數組容量不夠用,須要擴容了。

這裏拋出兩個問題:

擴容是怎麼擴的?

數組下標是怎麼來的?

先看問題一,怎麼擴容的?

看源碼:

怎麼樣,看到的第一眼你想到了什麼?

大聲的說出來,是否是想到了 HashMap 裏面的一段源碼?

和 HashMap 裏面的位運算殊途同歸。

在 InternalThreadLocalMap 中擴容就是變成原來大小的 2 倍。從 32 到 64,從 64 到 128 這樣。

擴容完成以後把原數組裏面的值拷貝到新的數組裏面去。

而後剩下的部分用 UNSET 填充。最後把咱們傳進來的 value 放到指定位置上。

再看看問題二,數組下標怎麼來的?也就是這個 index:

從上往下看,能夠看到最後,這個 index 本質上是一個 AtomicInteger 。

主要看一下標號爲 ① 的地方。

index 每次都是加一,對應的是 InternalThreadLocalMap 裏的數組下標。

第一眼看到的時候,裏面的 if 判斷 index<0 我是能夠理解的,防止溢出嘛。

可是下面在拋出異常以前,還調用了 decrementAndGet 方法,又把值減回去了。

你說這是爲何?

開始我沒想明白。可是有天晚上睡覺以前,電光火石一瞬間我想明白了。

若是不把值減回去,加一的代碼還在不斷的被調用,那麼這個 index 理論上講是有可能又被加到正數的,這一點你能明白吧?

爲何我說理論上呢?

int 的取值範圍是 [-2147483648 到 2147483647]。

若是 int 從 0 增長,一直溢出到 -2147483648,再從 -2147483648 加到 0,中間有 4294967295 個數字。

一個數字對應數組的一個下標,就算裏面放的是一個字節的 boolean 型,那麼大概也就是 4T 的內存吧。

因此,我以爲這是理論上的。

到這一步,咱們已經完成了從 Thread 裏面取出 InternalThreadLocalMap ,而且往裏面放數據的操做。

最後,InternalThreadLocal 的 set 方法只剩下最後一行代碼,咱們還沒說:

就是 setIndexedVariable 方法返回 true 後,會執行 addToVariablesToRemove 方法。

這個方法其實就是在數組的第一個位置維護當前線程裏面的全部的 InternalThreadLocalMap 。

這裏的關鍵點其實就是這個變量:

static final,能保證 VARIABLE_TO_REMOVE_INDEX 恆等於 0,也就是數組的第一個位置。

用示例程序,給你們演示一下,它第一個位置放的東西:

在第 21 行打上斷點,而後看一下執行完 addToVariablesToRemove 方法後,InternalThreadLocalMap 數組的狀況:

誠不欺你,第 0 個位置上放的是全部的 InternalThreadLocal 的集合。

因此,咱們看一下它的 size 方法,就能明白這裏爲何要減一了:

那麼在第一個位置維護線程裏面全部的 InternalThreadLocal 集合的用處是什麼?

看看它的 removeAll 方法:

直接從數組中取出第 0 個位置的數據,而後循環幹掉它就行。

set 方法就分析到這裏啦,算是保姆級的一行行手把手教學了吧。

藉助這個方法,也帶你們看了內部結構。

點到爲止。get 方法很簡單的,你們記得本身去看一下哦。

咱們再看一下此次 pr 提交的東西:

咱們看看這四個線程池有什麼變化:

就是換了工廠類。

換工廠類的目的是什麼呢?

newThread 的時候,new 的是 InternalThread 線程。

好一個偷天換日。

前面咱們說了,要用改造版的 ThreadLocal ,必需要配合 InternalThread 線程使用,不然就會退化爲原生的 ThreadLocal 。

其實, Dubbo 此次提交,改造的東西並很少。關鍵的、核心的代碼都是從 Netty 那邊 copy 過來的。

我這就是一個引子,你們能夠再去看看 Netty 的 FastThreadLocal 類。

關於此次 pr 提交

接下來又是 get 奇怪知識點的時刻了。

前面說了,這個 pr 裏面出場人物一個比一個「騷」,這一節我帶你們看一下,是怎麼個「騷」法。

https://github.com/apache/dubbo/pull/1745·

首先是 pr 的提交者,carryxyh 同窗的代碼在 2018 年 5 月 15 日的時候被 merge 了:

正常來講,carryxyh 同窗對於開源社區的一次貢獻就算是完美結束了,簡歷上又能夠濃墨重彩的寫上一小筆。

可是 15 天以後發生的事情,多是他作夢也想不到的。

那一天,一個叫作 normanmaurer 的哥們在這個 pr 下面說了一句話:

先無論他說的啥。

你知道他是誰嗎?他在我以前的文章中其實也出現過的。

他就是 Netty 的爸爸。

他是這樣說的:

他的意思就是說:

哥們,你這個東西我怎麼以爲是從 Netty 那邊弄過來的呢?本着開源的精神,你直接弄過來是沒有問題的,可是你至少得按照規矩辦事吧?得遵循 AL2 協議來。並且我甚至看到你在你的 pr 裏面提到了 Netty 。

至於這個 AL2 究竟是什麼,我是沒有看明白的。

可是不重要,我就把它理解爲一個給開源社區貢獻代碼時須要遵照的一個協議吧。

carryxyh 同窗看到 Netty 的爸爸找他了,很快就回復了兩條消息:

carryxyh同窗說道:

老哥,我在 javadoc 裏面提到了,個人靈感來源就是 Netty 的 FastThreadLocal 類。我寫這個的目的就是告訴全部看到這個類的朋友,這裏的大部分代碼來自 Netty。

那我除了在 javadoc 裏面寫上來源是 Netty 外,還須要作什麼嗎?還有你說的 AL2 是什麼東西,你能不能告訴我?

我必定會盡快修復的。

這麼一來一回,我大概明白這兩我的在說什麼了。

Netty 的爸爸說你用了個人代碼,這徹底沒有問題,可是你得遵循一個協議哦。

carryxyh 同窗說,我已經在 javadoc 裏說了我這部分代碼就是來自 Netty 的,我真不知道還該作什麼,請你告訴我。

Netty 的爸爸回覆了一個連接:

他說:你就看着這個連接,按照它整就行。

他發的這個連接我看了,怎麼說呢,很是的哇塞,純英文,內容很是的多。先不關注是啥吧,反正 carryxyh 同窗確定會認真閱讀的。

在 carryxyh 同窗沒有回覆以前,一個叫作 justinmclean 的哥們出來對 Netty 的爸爸說話了:


他說:實際上,ALv2 許可證已經不適用了,有新的政策出來了,以新的通知和許可證文件爲準。

這個哥們既然這樣輕描淡寫的說有新政策了。我潛意識就以爲他不是一個通常人,因而我查了一下:

主席、30年+、PMC、導師......

還愣着幹嗎,開始端茶吧。

大佬都出來了,接下來的對話大概也就是圍繞着怎麼纔是一次符合開源標準的提交。

主席說,到底需不須要聲明版權,得看代碼的改造點多很少。

Netty 的爸爸說:據我所知,除了包名和類名不同外,其餘的基本沒有變化。

最終 carryxyh 同窗說把 Netty 的 FastThreadLocal 的文件頭弄過來,是否是就完事了,

主席說:沒毛病,我就是這樣想的。

因此,咱們如今在 Dubbo 的 InternalThreadLocal 文件的最開始,還能夠看到這樣的 Netty 的說明:

這個東西,就是這樣來的,不是隨便寫的,是有講究。

好了,這應該是我全部文章中出現過的第 9 個用不上的傻吊知識點了吧。送給你,沒必要客氣。

好了,此次的文章就到這裏啦。

相關文章
相關標籤/搜索