揭開ThreadLocal的面紗

xinlmain發表於2019-03-31

當初使用C#時,研究過好一陣它的ThreadLocal,以及可以跨執行緒傳遞的LogicalCallContext(ExecutionContext),無奈C#不開源(所幸有了.Net Core),只能滿世界找文件,找部落格。切換到Java後,終於接觸到了另一種研究問題的方法:相比於查資料,更可以看程式碼,除錯程式碼。然後,一切都不那麼神祕了。

作用及核心原理

在我看來,Thread Local主要提供兩個功能:

  1. 方便傳參。提供一個方便的“貨架子”,想存就存,想取的時候能取到,不用每層方法呼叫都傳一大堆引數。(我們通常傾向於把公共的資料放到貨架子裡)
  2. 執行緒隔離。各個執行緒的值互不相干,遮蔽了多執行緒的煩惱。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)

程式碼的註釋太到位了。ThreadLocal應該翻譯為【執行緒本地變數】,意為和普通變數相對。ThreadLocal通常是一個靜態變數,但其get()得到的值在各個執行緒中互不相干。

ThreadLocal的幾個核心方法:

  • get() 得到變數的值。如果此ThreadLocal在當前執行緒中被設定過值,則返回該值;否則,間接地呼叫initialValue()初始化當前執行緒中的變數,再返回初始值。
  • set() 設定當前執行緒中的變數值。
  • protected initialValue() 初始化方法。預設實現是返回null。
  • remove() 刪除當前執行緒中的變數。

原理簡述

  • 每個執行緒都有一個 ThreadLocalMap 型別的 threadLocals 屬性,ThreadLocalMap 類相當於一個Map,key 是 ThreadLocal 本身,value 就是我們設定的值。
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
複製程式碼
  • 當我們通過 threadLocal.set(“猿天地”); 的時候,就是在這個執行緒中的 threadLocals 屬性中放入一個鍵值對,key 是 當前執行緒,value 就是你設定的值猿天地。
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
複製程式碼
  • 當我們通過 threadlocal.get() 方法的時候,就是根據當前執行緒作為key來獲取這個執行緒設定的值。
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
             @SuppressWarnings("unchecked")
             T result = (T)e.value;
             return result;
        }
    }
    return setInitialValue();
}
複製程式碼

thread-local.png

核心:ThreadLocalMap

ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.

ThreadLocalMap是一個定製的Hash map,使用開放定址法解決衝突。

  • 它的Entry是一個WeakReference,準確地說是繼承了WeakReference
  • ThreadLocal物件的引用被傳到WeakReferencereference中,entry.get()被當作map元素的key,而Entry還多了一個欄位value,用來存放ThreadLocal變數實際的值。
  • 由於是弱引用,若ThreadLocal物件不再有普通引用,GC發生時會將ThreadLocal物件清除。而Entry的key,即entry.get()會變為null。然而,GC只會清除被引用物件,Entry還被執行緒的ThreadLocalMap引用著,因而不會被清除。因而,value物件就不會被清除。除非執行緒退出,造成該執行緒的ThreadLocalMap整體釋放,否則value的記憶體就無法釋放,記憶體洩漏
  • JDK的作者自然想到了這一點,因此在ThreadLocalMap的很多方法中,呼叫expungeStaleEntries()清除entry.get() == null 的元素,將Entry的value釋放。
  • 然而,我們大部分的使用場景是,ThreadLocal是一個靜態變數,因此永遠有普通引用指向每個執行緒中的ThreadLocalMap的該entry。因此該ThreadLocal的Entry永遠不會被釋放,自然expungeStaleEntries()就無能為力,value的記憶體也不會被釋放。而在我們確實用完了ThreadLocal後,可以主動呼叫remove()方法,主動刪掉entry。

然而,真的有必要呼叫remove()方法嗎?通常我們的場景是服務端,執行緒在不斷地處理請求,每個請求到來會導致某執行緒中的Thread Local變數被賦予一個新的值,而原來的值物件自然地就失去了引用,被GC清理。所以不存在洩露

跨執行緒傳遞

Thread Local是不能跨執行緒傳遞的,執行緒隔離嘛!但有些場景中我們又想傳遞。例如:

  1. 啟動一個新執行緒執行某個方法,但希望新執行緒也能通過Thread Local獲取當前執行緒擁有的上下文(e.g., User ID, Transaction ID)。
  2. 將任務提交給執行緒池執行時,希望將來執行任務的那個執行緒也能繼承當前執行緒的Thread Local,從而可以使用當前的上下文。

下面我們就來看一下有哪些方法。

InheritableThreadLocal

原理:InheritableThreadLocal這個類繼承了ThreadLocal,重寫了3個方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 可以忽略
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
複製程式碼

可以看到使用InheritableThreadLocal時,map使用了執行緒的inheritableThreadLocals 欄位,而不是之前的threadLocals 欄位。

Thread的兩個欄位及註釋

inheritableThreadLocals 欄位既然叫可繼承的,自然在建立新執行緒的時候會傳遞。程式碼在Thread的init()方法中:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
複製程式碼

到此為止,通過inheritableThreadLocals我們可以在父執行緒建立子執行緒的時候將ThreadLocal中的值傳遞給子執行緒,這個特性已經能夠滿足大部分的需求了[1]。但是還有一個很嚴重的問題會出現線上程複用的情況下[2],比如執行緒池中去使用inheritableThreadLocals 進行傳值,因為inheritableThreadLocals 只是會在新建立執行緒的時候進行傳值,執行緒複用並不會做這個操作。

到這裡JDK就無能為力了。C#提供了LogicalCallContext(以及Execution Context機制)來解決,Java要解決這個問題就得自己去擴充套件執行緒類,實現這個功能。

阿里開源的transmittable-thread-local

GitHub地址

transmittable-thread-local使用方式分為三種:(裝飾器模式哦!)

  1. 修飾Runnable和Callable
  2. 修飾執行緒池
  3. Java Agent來修飾(執行時修改)JDK執行緒池實現類。

具體使用方式官方文件非常清楚。

下面簡析原理:

  • 既然要解決在使用執行緒池時的thread local傳遞問題,就要把任務提交時的當前ThreadLocal值傳遞到任務執行時的那個執行緒。
  • 而如何傳遞,自然是在提交任務前**捕獲(capture)當前執行緒的所有ThreadLocal,存下來,然後在任務真正執行時在目標執行緒中放出(replay)**之前捕獲的ThreadLocal。

程式碼層面,以修飾Runnable舉例:

  1. 建立TtlRunnable()時,一定先呼叫capture()捕獲當前執行緒中的ThreadLocal
private TtlCallable(@Nonnull Callable<V> callable, boolean releaseTtlValueReferenceAfterCall) {
    this.capturedRef = new AtomicReference<Object>(capture());
    ...
}
複製程式碼
  1. capture() 方法是Transmitter類的靜態方法:
public static Object capture() {
        Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
        for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
            captured.put(threadLocal, threadLocal.copyValue());
        }
        return captured;
}
複製程式碼
  1. run()中,先放出之前捕獲的ThreadLocal。
public void run() {
    Object captured = capturedRef.get();
    ...
    Object backup = replay(captured);
    try {
        runnable.run();
    } finally {
        restore(backup); 
    }
}
複製程式碼

時序圖:

完整時序圖

應用

  • Spring MVC的靜態類 RequestContextHoldergetRequestAttributes()實際上獲得的就是InheritableThreadLocal<RequestAttributes>在當前執行緒中的值。也可以說明它可以傳遞到自身建立的執行緒中,但對已有的執行緒無能為力。

    至於它是什麼什麼被設定的,可以參考其註釋:Holder class to expose the web request in the form of a thread-bound RequestAttributes object. The request will be inherited by any child threads spawned by the current thread if the inheritable flag is set to true. Use RequestContextListener or org.springframework.web.filter.RequestContextFilter to expose the current web request. Note that org.springframework.web.servlet.DispatcherServlet already exposes the current request by default.

  • Spring

  • 阿里巴巴TTL總結的幾個應用場景

...

一些坑

(未完待續,坑挖待填)

相關文章