TransmittableThreadLocal在使用線程池等會緩存線程的組件狀況下傳遞ThreadLocal

一、簡介

TransmittableThreadLocal 是Alibaba開源的、用於解決 「在使用線程池等會緩存線程的組件狀況下傳遞ThreadLocal」 問題的 InheritableThreadLocal 擴展。若但願 TransmittableThreadLocal 在線程池與主線程間傳遞,需配合 TtlRunnableTtlCallable 使用。html

二、使用場景

下面是幾個典型場景例子。java

  1. 分佈式跟蹤系統
  2. 應用容器或上層框架跨應用代碼給下層SDK傳遞信息
  3. 日誌收集記錄系統上下文

三、簡單分析使用

JDKInheritableThreadLocal類能夠完成父線程到子線程的值傳遞。但對於使用線程池等會池化複用線程的組件的狀況,線程由線程池建立好,而且線程是池化起來反覆使用的;這時父子線程關係的ThreadLocal值傳遞已經沒有意義,應用須要的其實是把 任務提交給線程池時ThreadLocal值傳遞到 任務執行時git

下面分析下InheritableThreadLocInheritableThreadLocal類重寫了ThreadLocal的3個函數:github

/**
  * 該函數在父線程建立子線程,向子線程複製InheritableThreadLocal變量時使用
*/
protected T childValue(T parentValue) {
    return parentValue;
}

/**
  * 因爲重寫了getMap,操做InheritableThreadLocal時,
  * 將隻影響Thread類中的inheritableThreadLocals變量,
  * 與threadLocals變量再也不有關係
*/
ThreadLocalMap getMap(Thread t) {
     return t.inheritableThreadLocals;
}

/**
 * 相似於getMap,操做InheritableThreadLocal時,
 * 將隻影響Thread類中的inheritableThreadLocals變量,
 * 與threadLocals變量再也不有關係
 */
void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

注意:重寫了getMap()和createMap()兩個函數,說到InheritableThreadLocal,還要從Thread類提及:bootstrap

public class Thread implements Runnable {
   ......(其餘源碼)
    /* 
     * 當前線程的ThreadLocalMap,主要存儲該線程自身的ThreadLocal
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal,自父線程集成而來的ThreadLocalMap,
     * 主要用於父子線程間ThreadLocal變量的傳遞
     * 本文主要討論的就是這個ThreadLocalMap
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ......(其餘源碼)
}

Thread類中包含 threadLocalsinheritableThreadLocals 兩個變量,其中 inheritableThreadLocals 即主要存儲可自動向子線程中傳遞的ThreadLocal.ThreadLocalMap。
接下來看一下父線程建立子線程的流程,咱們從最簡單的方式提及:api

用戶建立Thread緩存

hread thread = new Thread();
**
 * Allocates a new {@code Thread} object. This constructor has the same
 * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
 * {@code (null, null, gname)}, where {@code gname} is a newly generated
 * name. Automatically generated names are of the form
 * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
 */
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

Thread初始化oracle

/**
 * 默認狀況下,設置inheritThreadLocals可傳遞
 */
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}

/**
 * 初始化一個線程.
 * 此函數有兩處調用,
 * 一、上面的 init(),不傳AccessControlContext,inheritThreadLocals=true
 * 二、傳遞AccessControlContext,inheritThreadLocals=false
 */
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);

    ......(其餘代碼)
}

能夠看到,採用默認方式產生子線程時,inheritThreadLocals=true;若此時父線程inheritableThreadLocals不爲空,則將父線程inheritableThreadLocals傳遞至子線程。框架

讓咱們繼續追蹤createInheritedMap分佈式

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}


/**
 * 構建一個包含全部parentMap中Inheritable ThreadLocals的ThreadLocalMap
 * 該函數只被 createInheritedMap() 調用.
 */
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    // ThreadLocalMap 使用 Entry[] table 存儲ThreadLocal
    table = new Entry[len];

    // 逐一複製 parentMap 的記錄
    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,而不是直接賦值,
                // 畢竟childValue內部也是直接將e.value返回;
                // 我的理解,主要爲了減輕閱讀代碼的難度
                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++;
            }
        }
    }
}

從ThreadLocalMap可知,子線程將parentMap中的全部記錄逐一複製至自身線程。InheritableThreadLocal主要用於子線程建立時,須要自動繼承父線程的ThreadLocal變量,方便必要信息的進一步傳遞。

接下來提供的TransmittableThreadLocal類繼承並增強InheritableThreadLocal類,解決上述的問題。

使用類TransmittableThreadLocal來保存值,並跨線程池傳遞。

TransmittableThreadLocal繼承InheritableThreadLocal,使用方式也相似。

相比InheritableThreadLocal,添加了

  1. protected方法copy
    用於定製 任務提交給線程池時 的ThreadLocal值傳遞到 任務執行時 的拷貝行爲,缺省傳遞的是引用。
  2. protected方法beforeExecute/afterExecute
    執行任務(Runnable/Callable)的前/後的生命週期回調,缺省是空操做。

1. 簡單使用

父線程給子線程傳遞值。

示例代碼:

// 在父線程中設置
TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

// =====================================================

// 在子線程中能夠讀取,值是"value-set-in-parent"
String value = parent.get();

這是實際上是InheritableThreadLocal的功能,應該使用InheritableThreadLocal來完成。

但對於使用線程池等會池化複用線程的組件的狀況,線程由線程池建立好,而且線程是池化起來反覆使用的;這時父子線程關係的ThreadLocal值傳遞已經沒有意義,應用須要的其實是把 任務提交給線程池時ThreadLocal值傳遞到 任務執行時

解決方法參見下面的這幾種用法。

2. 保證線程池中傳遞值

2.1 修飾RunnableCallable

使用TtlRunnableTtlCallable來修飾傳入線程池的RunnableCallable

示例代碼:

TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

Runnable task = new Task("1");
// 額外的處理,生成修飾了的對象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);

// =====================================================

// Task中能夠讀取,值是"value-set-in-parent"
String value = parent.get();

上面演示了RunnableCallable的處理相似

TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

Callable call = new Call("1");
// 額外的處理,生成修飾了的對象ttlCallable
Callable ttlCallable = TtlCallable.get(call);
executorService.submit(ttlCallable);

// =====================================================

// Call中能夠讀取,值是"value-set-in-parent"
String value = parent.get();

整個過程的完整時序圖

時序圖

2.2 修飾線程池

省去每次RunnableCallable傳入線程池時的修飾,這個邏輯能夠在線程池中完成。

經過工具類com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法:

  • getTtlExecutor:修飾接口Executor
  • getTtlExecutorService:修飾接口ExecutorService
  • getTtlScheduledExecutorService:修飾接口ScheduledExecutorService

示例代碼:

ExecutorService executorService = ...
// 額外的處理,生成修飾了的對象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);

TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

Runnable task = new Task("1");
Callable call = new Call("2");
executorService.submit(task);
executorService.submit(call);

// =====================================================

// Task或是Call中能夠讀取,值是"value-set-in-parent"
String value = parent.get();

2.3 使用Java Agent來修飾JDK線程池實現類

這種方式,實現線程池的傳遞是透明的,代碼中沒有修飾Runnable或是線程池的代碼。便可以作到應用代碼 無侵入
# 關於 無侵入 的更多說明參見文檔Java Agent方式對應用代碼無侵入

示例代碼:

// ## 1. 框架上層邏輯,後續流程框架調用業務 ##
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>();
context.set("value-set-in-parent");

// ## 2. 應用邏輯,後續流程業務調用框架下層邏輯 ##
ExecutorService executorService = Executors.newFixedThreadPool(3);

Runnable task = new Task("1");
Callable call = new Call("2");
executorService.submit(task);
executorService.submit(call);

// ## 3. 框架下層邏輯 ##
// Task或是Call中能夠讀取,值是"value-set-in-parent"
String value = context.get();

Demo參見AgentDemo.kt。執行工程下的腳本scripts/run-agent-demo.sh便可運行Demo。

目前TTL Agent中,修飾了JDK中的線程池實現以下:

  1. java.util.concurrent.ThreadPoolExecutor 和 java.util.concurrent.ScheduledThreadPoolExecutor
    修飾實現代碼在TtlExecutorTransformlet.java
  2. java.util.concurrent.ForkJoinTask(對應的線程池組件是java.util.concurrent.ForkJoinPool
    修飾實現代碼在TtlForkJoinTransformlet.java
  3. java.util.TimerTask的子類(對應的線程池組件是java.util.Timer
    修飾實現代碼在TtlTimerTaskTransformlet.java
    注意:缺省沒有開啓TimerTask的修飾,使用Agent參數ttl.agent.enable.timer.task開啓:-javaagent:path/to/transmittable-thread-local-2.x.x.jar=ttl.agent.enable.timer.task:true
    更多關於TTL Agent參數的配置說明詳見TtlAgent.java的JavaDoc

關於java.util.TimerTask/java.util.Timer

TimerJDK 1.3的老類,不推薦使用Timer類。

推薦用ScheduledExecutorService
ScheduledThreadPoolExecutor實現更強壯,而且功能更豐富。 如支持配置線程池的大小(Timer只有一個線程);TimerRunnable中拋出異常會停止定時執行。更多說明參見10. Mandatory Run multiple TimeTask by using ScheduledExecutorService rather than Timer because Timer will kill all running threads in case of failing to catch exceptions. - Alibaba Java Coding Guidelines

關於boot class path設置

由於修飾了JDK的標準庫的類,標準庫由bootstrap class loader加載;上面修飾後的JDK類引用了TTL的代碼,因此TTLJar須要加到boot class path上。

TTLv2.6.0開始,加載TTL Agent會自動把本身的Jar設置到boot class path上。

注意:不能修改從Maven庫下載的TTLJar的文件名(形如transmittable-thread-local-2.x.x.jar)。 若是修改了,則須要本身手動經過-Xbootclasspath JVM參數來顯式配置(就像TTL以前的版本的作法同樣)。

實現是經過指定TTL Java Agent Jar文件裏manifest文件(META-INF/MANIFEST.MF)的Boot-Class-Path屬性:

Boot-Class-Path

A list of paths to be searched by the bootstrap class loader. Paths represent directories or libraries (commonly referred to as JAR or zip libraries on many platforms). These paths are searched by the bootstrap class loader after the platform specific mechanisms of locating a class have failed. Paths are searched in the order listed.

Java的啓動參數配置

Java的啓動參數加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar

若是修改了下載的TTLJar的文件名(transmittable-thread-local-2.x.x.jar),則須要本身手動經過-Xbootclasspath JVM參數來顯式配置:
好比修改文件名成ttl-foo-name-changed.jar,則還加上Java的啓動參數:-Xbootclasspath/a:path/to/ttl-foo-name-changed.jar

Java命令行示例以下:

java -javaagent:path/to/transmittable-thread-local-2.x.x.jar \
    -cp classes \
    com.alibaba.ttl.threadpool.agent.demo.AgentDemo

或是

java -javaagent:path/to/ttl-foo-name-changed.jar \
    -Xbootclasspath/a:path/to/ttl-foo-name-changed.jar \
    -cp classes \
    com.alibaba.ttl.threadpool.agent.demo.AgentDemo

Maven依賴

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.10.2</version>
</dependency>
相關文章
相關標籤/搜索