標籤:

線程之間傳遞 ThreadLocal 對象

為了介紹線程間傳遞 ThreadLocal 對象這個事情,請先耐心一些跟我一起來看看我是怎麼遇到線程間傳遞 ThreadLocal 對象這個需求的。

一起看這麼個場景,大致上是下面這樣,是 clojure 的代碼。但是請不要擔心,它非常短也非常簡單:

;; 定義一個動態綁定的變數,實際就是個 Java ThreadLocal 對象(defonce ^:dynamic *utc* false);; 處理某 Http 請求的函數, 參數 req 內是用戶傳來的 Key -> Value 格式的參數(defn some-http-handler-function [req] ;; 首先需要從 req 內讀取 API 版本信息 所有請求都會有這個參數 ;; 如果 API 版本是 1.1 則認為使用的是 utc 時間,所以給 *utc* 變數綁定一個 true 值 (binding [*utc* (= (:api-version req) "1.1")] ;; 綁定好 *utc* 後,開始處理 req 請求,process 返回值作為 Http 請求的結果 (process req))

some-http-handler-function 是一個處理 HTTP 請求的函數,這個 Http 請求的 content-type 是 JSON。Server 收到請求後會將 body 內的 JSON 參數轉換為 Key -> Value 的 map 當做 req 參數傳入 some-http-handler-function 函數做處理。

我們的 Http 介面要求請求中必須帶著一些時間參數,並且時間參數須是符合 iso8601 格式的字元串,形如 2017-09-23T12:15:42.972Z。我們的 API 分為很多版本,1.1 版本之前的 API 沒有對用戶提供的這個時間參數的時區做約定,所以我們默認以當前伺服器所在時區來解析用戶傳來的時間參數。1.1 版本之後的 API,我們跟用戶約定時間參數必須是 UTC 時間,伺服器也就直接以 UTC 時間來解析時間參數,從而不再有時區差異問題。

因為相同的 some-http-handler-function 函數,要兼容處理 1.1 版本 API 請求和老版本 API 請求,所以要在請求中帶著 API 版本信息,並在收到請求後,根據 req 中的 API 版本信息,判斷是否使用 UTC 時間。因為處理請求的函數有多層嵌套,比如 (process req) 可能調用 (do-process req)、(do-after-do-process req)等等,又不想讓所有嵌套的函數調用都帶著是否使用的 UTC 時間的參數,就簡單地將是否使用 UTC 時間這個事情記錄在一個動態綁定的 *utc* 參數上。

如果不了解 clojure,可以將 *utc* 簡單理解為一個全局的 Java ThreadLocal 對象,同一個線程存入一個值後,比如將 *utc* 這個字元串綁定為 true 後,在該線程後續調用的所有函數、方法內,都能直接拿到 *utc* 參數的值,從而不用在所有該線程調用的函數、方法內都帶著 *utc* 參數。讓代碼簡單一些。

最初 (process req) 是個同步的調用,綁定完 *utc* 參數的主線程會完成 process 函數內所有邏輯,完成 Http 請求的處理並負責將結果發給用戶。但後來為了隔離,將 process 函數內增加了線程池,會從線程池找一個空閑線程來實際處理 Http 請求,當前主線程會 Block 住等待 process 執行結果。由於 Java ThreadLocal 對象是不能在線程之間傳遞的,所以主線程雖然綁定了 *utc* 參數,但是 process 內的業務線程並不知道這個事情,於是出現即使在處理 1.1 版本 API 的請求時,所有時間參數也均採用伺服器本地時區來解析,而不是 UTC 來解析,造成了 Bug。

這就是我遇到的線程之間傳遞 ThreadLocal 對象需求的來源。我們的解決辦法很簡單粗暴,就是將 *utc* 這個參數直接放在 process 函數的參數之中,等到 process 內的線程實際運行時,重新為線程池內實際執行 process 工作的業務線程綁定 *utc* 參數。

當時就在想如果有機制能在一些特定情況之下,讓 ThreadLocal 對象綁定的 Value 在不同線程之間能共享,對上面這種場景處理就會比較方便。主線程將處理請求的任務交給業務線程之後,即使兩個線程共享 *utc* 參數,但因為都不修改這個參數值,所以並不會引起問題。

下面我們看看這種 ThreadLocal 對象綁定值如何在不同線程之間傳遞。

父子進程之間傳遞 ThreadLocal 對象

這個實際上 JDK 自己有實現,是 InheritableThreadLocal 類。慢慢介紹一下它的原理。

Thread 類內實際有兩個 ThreadLocal.ThreadLocalMap,一個是給 ThreadLocal 使用的,還一個是給 InheritableThreadLocal 使用的。給 InheritableThreadLocal 使用的 ThreadLocalMap 特殊一些,在一個線程 fork 一個新的子線程的時候,父線程會檢查自己 Thread 內為 InheritableThreadLocal 使用的 ThreadLocalMap 是否為空,不為空則將其拷貝給子線程的為 InheritableThreadLocal 使用的 ThreadLocalMap。Thread 的 init 可以看到這塊邏輯:

if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

ThreadLocal.createInheritedMap 會調用 ThreadLocalMap 的拷貝構造函數實現大致為:

private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for each Entry e in parentTable { if e != null { ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { Object value = key.childValue(e.value); setKeyValueToThisThreadLocalMap(key, value); } } }}

這麼一來創建子線程之後,父線程的給 InheritableThreadLocal 使用的 ThreadLocalMap 就在子線程中有了一個副本。默認情況下父子線程的 ThreadLocalMap 內的 key 都指向同一個 InheritableThreadLocal 對象,Value 也指向同一個 Value。從上面能看到子線程存儲 ThreadLocalMap 的 Value 實際上是 key.childValue(e.value) 就是說能在使用 InheritableThreadLocal 的時候覆蓋 childValue 方法從而根據父線程的 Value 提供子線程的 Value。

但是對於開篇那個問題的場景,InheritableThreadLocal 無法使用。在那個場景中,相當於是一個 Thread A 發配一個 Runnable 或者 Callable 到一個線程池中,讓線程池內的線程去執行 Runnable/Callable。Thread A 和線程池內的線程並沒有父子關係,所以 Thread A 不能將綁定的 InheritableThreadLocal 值傳遞給線程池內負責實際執行 Runnable/Callable 的線程。

Thread A 傳遞給線程池內線程的只有 Runnable/Callable,所以如果想要實現將 ThreadA 的 ThreadLocal 值傳遞給線程池內實際負責執行 Runnable/Callable 的 Thread,就一定是需要在 Runnalbe/Callable 做文章,傳遞 Runnable/Callable 的時候將 Thread A 的 ThreadLocal 值存到 Runnable/Callable 中,之後線程池線程,比如是 Thread 1,在運行 Runnable/Callable 時從中讀出這些 ThreadLocal 值並存入Thread 1 的 ThreadLocalMap 中,實現 ThreadLocal 對象值的傳遞。這也是 [GitHub - alibaba/transmittable-thread-local](alibaba/transmittable-thread-local) 庫最基礎的實現原理。

感覺很多東西都是這樣,仔細想想原理感覺不難,但難的是能想到這個點子。之前遇到開篇問題的時候只是想繞過的笨辦法,沒想想怎麼從 ThreadLocal 原理上來解決這個問題。下面記錄一下這個庫的部分實現,但是這裡不準備直接記錄 transmittable thread local 是怎麼實現,而是看看能不能在現有了解東西的基礎上,一步一步推測出來它怎麼實現。

transmittable-thread-local

通過上面的描述,應該已對 transmittable thread local 的實現原理有個大致的猜想。首先是需要有個專門的 Runnable 或 Callable,用於讀取原 Thread 的 ThreadLocal 對象及其值並存在 Runnable/Callable 中,在執行 run 或者 call 的時候將存在 Runnable/Callable 中的 ThreadLocal 對象和值讀出來,存入調用 run 或 call 的線程。

讀取原 Thread 上所有的(或者說會發生這種線程間傳遞的)ThreadLocal 對象及其值比較麻煩,ThreadLocal 和 InheritableThreadLocal 都沒有開放內部的 ThreadLocalMap,不能直接讀取。所以要麼自己完全實現一套 ThreadLocalMap 機制,像 Netty 的 FastThreadLocal 那樣;要麼就是自己實現 ThreadLocal 的子類,在每次調用 ThreadLocal 的 set/get/remove 等介面的時候,為 Thread 記錄到底綁定了哪些需要發生線程間傳遞的 ThreadLocal 對象。後者更簡單和更可靠一些,所以可能選擇後者更穩妥,transmittable-thread-local 這個庫也是這麼做的。

通過 Runnable/Callable 傳遞 ThreadLocal 對象及其值的方法是有了,父子線程之間傳遞可以復用 InheritableThreadLocal 的實現,所以新的 TransmittableThreadLocal 對象需要繼承 InheritableThreadLocal 從而獲取它的父子線程間傳遞 ThreadLocal 對象及其值的能力。到目前為止,大致的類圖如下:

使用的時候必須使用使用 TransmittableThreadLocal,創建 Runnable 或 Callable 也必須使用 TtlCallable 或者 TtlRunnable。TransmittableThreadLocal 覆蓋實現了 ThreadLocal 的 set、get、remove,實際存儲 ThreadLocal 值的工作還是 ThreadLocal 父類完成,TransmittableThreadLocal 只是為每個使用它的 Thread 單獨記錄一份存儲了哪些 TransmittableThreadLocal 對象。拿 set 來說就是這個樣子:

public final void set(T value) { super.set(value); if (null == value) removeValue(); else addValue();}

addValue 和 removeValue 都是將當前 TransmittableThreadLocal 對象存入 TransmittableThreadLocal 內一個 static 的並且是 InheritableThreadLocal 的 WeakHashMap 中。

private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder = new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() { @Override protected Map<TransmittableThreadLocal<?>, ?> initialValue() { return new WeakHashMap<TransmittableThreadLocal<?>, Object>(); } @Override protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) { return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue); } };private void addValue() { if (!holder.get().containsKey(this)) { holder.get().put(this, null); // WeakHashMap supports null value. }}

相當於是和 Thread 綁定的所有 TransmittableThreadLocal 對象都保存在這個 holder 中,TransmittableThreadLocal 對應的 Value 則還是保存在 ThreadLocal 的 ThreadLocalMap 中。holder 只是為了記錄當前 Thread 綁定了哪些 TransmittableThreadLocal 對象,好在 TtlRunnable 或 TtlCallable 構造的時候通過 holder 取出這些 TransmittableThreadLocal 存入 TtlRunnable 或 TtlCallable。

剩下的更多細節感覺就不用再多記錄了,有興趣的話可以下載這個庫的代碼看看。上面原理雖然看上去簡單,但是這個庫還在易用性上做了很多增強,還是值得看看值得使用一下的。

頭圖是隨便找的,具體來自:Regifting 101


推薦閱讀:

從 HTTP 0.9 到 QUIC
QPS 和並發:如何衡量伺服器端性能
BaaS 服務的興起減少了後端的工作量,這意味著未來大批後台程序員要失業么?
Google 收購的 Firebase 相比 Parse、LeanCloud 怎麼樣?

TAG:LeanCloud | Java |