這是why哥的第 70 篇原創文章java
故事得從前段時間翻閱 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 類。
接下來又是 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 個用不上的傻吊知識點了吧。送給你,沒必要客氣。
好了,此次的文章就到這裏啦。