首頁 > 軟體

java開發工作中對InheritableThreadLocal使用思考

2022-11-15 14:00:15

引言

最近在工作中結合執行緒池使用 InheritableThreadLocal 出現了獲取執行緒變數“錯誤”的問題,看了相關的檔案和原始碼後在此記錄。

1. 先說結論

InheritableThreadLocal 只有在父執行緒建立子執行緒時,在子執行緒中才能獲取到父執行緒中的執行緒變數;

當配合執行緒池使用時:“第一次線上程池中開啟執行緒,能在子執行緒中獲取到父執行緒的執行緒變數,而當該子執行緒開啟之後,發生執行緒複用,該子執行緒仍然保留的是之前開啟它的父執行緒的執行緒變數,而無法獲取當前父執行緒中新的執行緒變數”,所以會發生獲取執行緒變數錯誤的情況。

2. 實驗例子

  • 建立一個執行緒數固定為1的執行緒池,先在main執行緒中存入變數1,並使用執行緒池開啟新的執行緒列印輸出執行緒變數,之後更改main執行緒的執行緒變數為變數2,再使用執行緒池中執行緒(發生執行緒複用)列印輸出執行緒變數,對比兩次輸出的值是否不同
/**
 * 測試執行緒池下InheritableThreadLocal執行緒變數失效的場景
 */
public class TestInheritableThreadLocal {
    private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    // 固定大小的執行緒池,保證執行緒複用
    private static final ExecutorService executorService = Executors.newFixedThreadPool(1);
    public static void main(String[] args) {
        threadLocal.set("main執行緒 變數1");
        // 正常取到 main執行緒 變數1
        executorService.execute(() -> System.out.println(threadLocal.get()));
        threadLocal.set("main執行緒 變數2");
        // 執行緒複用再取還是 main執行緒 變數1
        executorService.execute(() -> System.out.println(threadLocal.get()));
    }
}

輸出結果:

main執行緒 變數1 main執行緒 變數1

發現兩次輸出結果值相同,證明發生執行緒複用時,子執行緒獲取父執行緒變數失效

3. 詳解

3.1 JavaDoc

This class extends ThreadLocal to provide inheritance of values from parent thread to child thread: when a child thread is created, the child receives initial values for all inheritable thread-local variables for which the parent has values. Normally the child's values will be identical to the parent's; however, the child's value can be made an arbitrary function of the parent's by overriding the childValue method in this class. Inheritable thread-local variables are used in preference to ordinary thread-local variables when the per-thread-attribute being maintained in the variable (e.g., User ID, Transaction ID) must be automatically transmitted to any child threads that are created.

InheritableThreadLocal 繼承了 ThreadLocal, 以能夠讓子執行緒能夠從父執行緒中繼承執行緒變數: 當一個子執行緒被建立時,它會接收到父執行緒中所有可繼承的變數。通常情況下,子執行緒和父執行緒中的執行緒變數是完全相同的,但是可以通過重寫 childValue 方法來使父子執行緒中的值不同。

當執行緒中維護的變數如UserId, TransactionId 等必須自動傳遞到新建立的任何子執行緒時,使用InheritableThreadLocal要優於ThreadLocal

3.2 原始碼

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * 當子執行緒被建立時,通過該方法來初始化子執行緒中執行緒變數的值,
     * 這個方法在父執行緒中被呼叫,並且在子執行緒開啟之前。
     * 
     * 通過重寫這個方法可以改變從父執行緒中繼承過來的值。
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

其中childValue方法來獲取父執行緒中的執行緒變數的值,也可通過重寫這個方法來將獲取到的執行緒變數的值進行修改。

getMap方法和createMap方法中,可以發現inheritableThreadLocals變數,它是 ThreadLocalMap,在Thread類

3.2.1 childValue方法

  • 開啟新執行緒時,會呼叫Thread的構造方法
    public Thread(ThreadGroup group, String name) {
        init(group, null, name, 0);
    }
  • 沿著構造方法向下,找到init方法的最終實現,其中有如下邏輯:為當前執行緒建立執行緒變數以繼承父執行緒中的執行緒變數
/**
 * @param inheritThreadLocals 為ture,代表是為 包含可繼承的執行緒變數 的執行緒進行初始化
 */
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    ...
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        // 注意這裡建立子執行緒的執行緒變數
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ...
}

ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)建立子執行緒 InheritedMap 的具體實現

createInheritedMap 方法,最終會呼叫到 ThreadLocalMap私有構造方法,傳入的引數parentMap即為父執行緒中儲存的執行緒變數

    private ThreadLocalMap(ThreadLocalMap parentMap) {
        Entry[] parentTable = parentMap.table;
        int len = parentTable.length;
        setThreshold(len);
        table = new Entry[len];
        for (int j = 0; j < len; j++) {
            Entry e = parentTable[j];
            if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {
                    // 注意!!! 這裡呼叫了childValue方法
                    Object value = key.childValue(e.value);
                    Entry c = new Entry(key, value);
                    int h = key.threadLocalHashCode & (len - 1);
                    while (table[h] != null)
                        h = nextIndex(h, len);
                    table[h] = c;
                    size++;
                }
            }
        }
    }

這個方法會對父執行緒中的執行緒變數做深拷貝,其中呼叫了childValue方法來獲取/初始化子執行緒中的值,並儲存到子執行緒中

  • 由上可見,可繼承的執行緒變數只是線上程被建立的時候進行了初始化工作,這也就能解釋為什麼線上程池中發生執行緒複用時不能獲取到父執行緒執行緒變數的原因

4. 實驗例子流程圖

  • main執行緒set main執行緒 變數1時,會呼叫到InheritableThreadLocalcreateMap方法,建立 inheritableThreadLocals 並儲存執行緒變數
  • 開啟子執行緒1時,會深拷貝父執行緒中的執行緒變數到子執行緒中,如圖示
  • main執行緒set main執行緒 變數2,會覆蓋主執行緒中之前set的mian執行緒變數1
  • 最後發生執行緒複用,子執行緒1無法獲取到main執行緒新set的值,仍然列印 main執行緒 變數1

以上就是java開發工作中對InheritableThreadLocal使用思考的詳細內容,更多關於java開發InheritableThreadLocal的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com