1. 程式人生 > >這玩意比ThreadLocal叼多了,嚇得why哥趕緊分享出來。

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

這是why哥的第 70 篇原創文章 # 從Dubbo的一次提交開始 故事得從前段時間翻閱 Dubbo 原始碼時,看到的一段程式碼講起。 這段程式碼就是這個: `org.apache.dubbo.rpc.RpcContext` ![](https://img2020.cnblogs.com/blog/1820785/202010/1820785-20201019131941535-1877494176.png) 使用 InternalThreadLocal 提升效能。 相信作為一個程式猿,都會被 improve performance(提升效能)這樣的字眼抓住眼球。 心裡開始癢癢的,必須要一探究竟。 剛看到這段程式碼的時候,我就想:既然他是要提升效能,那說明之前的東西表現的不太好。 那之前的東西是什麼? ![](https://img2020.cnblogs.com/blog/1820785/202010/1820785-20201019125125620-814169922.png) 經過長時間的推理、縝密的分析,我大膽的猜測到之前的東西就是:ThreadLocal。 來,帶大家看一下: ![](https://img2020.cnblogs.com/blog/1820785/202010/1820785-20201019125207901-350877255.png) 果不其然,我真是太厲害了。 2018 年 5 月 15 日的提交:New threadLocal provides more performance. (#1745) 可以看到這次提交的後面跟了一個數字:1745。它對應一個 pr,連結如下: `https://github.com/apache/dubbo/pull/1745` 在這個 pr 裡面還是有很多有趣的東西的,出場人物一個比一個騷,文章的最後帶大家看看。 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/243d477418044fe1ad39fb812546f8fb~tplv-k3u1fbpfcp-watermark.image) # 能幹啥用? 在說 ThreadLocal 和 InternalThreadLocal 之前,還是先講講它們是幹啥用的吧。 InternalThreadLocal 是 ThreadLocal 的增強版,所以他們的用途都是一樣的,一言蔽之就是:傳遞資訊。 你想象你有一個場景,呼叫鏈路非常的長。當你在其中某個環節中查詢到了一個數據後,最後的一個節點需要使用一下。 這個時候你怎麼辦?你是在每個介面的入參中都加上這個引數,傳遞進去,然後只有最後一個節點用嗎? 可以實現,但是不太優雅。 你再想想一個場景,你有一個和業務沒有一毛錢關係的引數,比如 traceId ,純粹是為了做日誌追蹤用。 你加一個和業務無關的引數一路透傳幹啥玩意? 通常我們的做法是放在 ThreadLocal 裡面,作為一個全域性引數,在當前執行緒中的任何一個地方都可以直接讀取。當然,如果你有修改需求也是可以的,視需求而定。 絕大部分的情況下,ThreadLocal 是適用於讀多寫少的場景中。 舉三個框架原始碼中的例子,大家品一品。 **第一個例子:Spring 的事務。** 在我的早期作品[《事務沒回滾?來,我們從現象到原理一起分析一波》](https://mp.weixin.qq.com/s/JttPiwDcPYxkDtpQEMmi9Q)裡面,我曾經寫過: Spring 的事務是基於 AOP 實現的,AOP 是基於動態代理實現的。所以 @Transactional 註解如果想要生效,那麼其呼叫方,需要是被 Spring 動態代理後的類。 因此如果在同一個類裡面,使用 this 呼叫被 @Transactional 註解修飾的方法時,是不會生效的。 為什麼? 因為 this 物件是未經動態代理後的物件。 那麼我們怎麼獲取動態代理後的物件呢? 其中的一個方法就是通過 AopContext 來獲取。 ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/156ed61f6c9b44f7b61d3a4271dee2c6~tplv-k3u1fbpfcp-watermark.image) 其中第三步是這樣獲取的:AopContext.currentProxy(); 然後我還非常高冷的(咦,想想就覺得羞恥)說了句:對於 AopContext 我多說幾句。 看一下 AopContext 裡面的 ThreadLocal: ![](https://img2020.cnblogs.com/blog/1820785/202010/1820785-20201019125241565-2008822618.png) 呼叫 currentProxy 方法時,就是從 ThreadLocal 裡面獲取當前類的代理類。 那他是怎麼放進去的呢? 我高冷的第二句是這樣說的: ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5573079c1b284a5587f616dfcac01214~tplv-k3u1fbpfcp-watermark.image) 對應的程式碼位置如下: ![](https://img2020.cnblogs.com/blog/1820785/202010/1820785-20201019125327707-1334428173.png) 可以看到,經過一個 if 判斷,如果為 true ,則呼叫 AopContext.setCurrentProxy 方法,把代理物件放到 AopContext 裡面去。 而這個 if 判斷的配置預設是 false,所以需要通過剛剛說的配置修改為 true,這樣 AopContext 才會生效。 附送一個知識點給你,不客氣。 ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ed023ce9c1494e2db2682a83c7a2e953~tplv-k3u1fbpfcp-watermark.image) **第二個例子:mybatis 的分頁外掛,PageHelper。** 使用方法非常簡單,從官網上截個圖: ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc634a71d7604babab46b17add544cff~tplv-k3u1fbpfcp-watermark.image) 這裡它為什麼說:緊跟著的第一個 select 方法會被分頁。 或者說:什麼情況下會導致不安全的分頁? 來,就當是一個面試題,並且我給你提示了:從 ThreadLocal 的角度去回答。 其實就是因為 PageHelper 方法使用了靜態的 ThreadLocal 引數,分頁引數和執行緒是繫結的: ![](https://img2020.cnblogs.com/blog/1820785/202010/1820785-20201019125354825-590066688.png) 如果我們寫出下面這樣的程式碼,就是不安全的用法: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/547d4e4d38994b11a432ea87ef546a04~tplv-k3u1fbpfcp-watermark.image) 這種情況下由於 param1 存在 null 的情況,就會導致 PageHelper 生產了一個分頁引數,但是沒有被消費,這個引數就會一直保留在這個執行緒上,也就是放線上程的 ThreadLocal 裡面。 當這個執行緒再次被使用時,就可能導致不該分頁的方法去消費這個分頁引數,這就產生了莫名其妙的分頁。 上面這個程式碼,應該寫成下面這個樣子: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f5d31a3dba544977be9d0655c7c46a56~tplv-k3u1fbpfcp-watermark.image) 這種寫法,就能保證安全。 核心思想就一句話:只要你可以保證在 PageHelper 方法呼叫後緊跟 MyBatis 查詢方法,這就是安全的。 因為 PageHelper 在 finally 程式碼段中自動清除了 ThreadLocal 儲存的物件。 就算程式碼在進入 Executor 前發生異常,導致執行緒不可用的情況,比如常見的介面方法名稱和 XML 中的不匹配,導致找不到 MappedStatement ,由於自動清除,也不會導致 ThreadLocal 引數被錯誤的使用。 所以,我看有的人為了保險起見這樣去寫: ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90ae054e295946c9ac3377f19b765461~tplv-k3u1fbpfcp-watermark.image) 怎麼說呢,這個程式碼.... ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6476086a38074f9ab623760de7a40a23~tplv-k3u1fbpfcp-watermark.image) **第三個例子:Dubbo 的 RpcContext。** RpcContext 這個物件裡面維護了兩個 InternalThreadLocal,分別是存放 local 和 server 的上下文。 也就是我們說的增強版的 ThreadLocal: ![](https://img2020.cnblogs.com/blog/1820785/202010/1820785-20201019125451716-1912228620.png) 作為一個 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 上: ![](https://img2020.cnblogs.com/blog/1820785/202010/1820785-20201019125509514-1958239399.png) InternalThreadLocal 是 ThreadLocal 的一個變種,當配合 InternalThread 使用時,具有比普通 Thread 更高的訪問效能。 **InternalThread 的內部使用的是陣列,通過下標定位,非常的快。如果遇得擴容,直接陣列擴大一倍,完事。** **而 ThreadLocal 的內部使用的是 hashCode 去獲取值,多了一步計算的過程,而且用 hashCode 必然會遇到 hash 衝突的場景,ThreadLocal 還得去解決 hash 衝突,如果遇到擴容,擴容之後還得 rehash ,這可不得慢嗎?** 資料結構都不一樣了,這其實就是這兩個類的本質區別,也是 InternalThread 的效能在 Dubbo 的這個場景中比 ThreadLocal 好的根本原因。 而 InternalThread 這個設計思想是從 Netty 的 FastThreadLocal 中學來的。 本文主要聊聊 InternalThread ,但是我希望的是大家能學到這個類的思想,而不是用法。 首先,我們先搞個測試類: ``` public class InternalThreadLocalTest { private static InternalThr