解決ThreadLocal在開啓子線程時,父線程向子線程值傳遞問題,源碼分析

有關ThreadLocal的介紹我之前一篇文章已經做了介紹:https://blog.csdn.net/qq_26012495/article/details/86475725

本篇主要解決,在父線程中開啓子線程時ThreadLocal存在的值傳遞問題,以及如何解決。

 

目錄

 

一、ThreadLocal

1. 普通ThreadLocal存在的問題

二、InheritableThreadLocal 

1. InheritableThreadLocal源碼分析

2. Thread類源碼分析

 3. 修改爲InheritableThreadLocal代碼測試

4. InheritableThreadLocal在其他場景存在的問題

三、TransmittableThreadLocal(阿里開源)

1. 查看TransmittableThreadLocal源碼

2. TtlExecutors類使用及源碼分析


一、ThreadLocal

1. 普通ThreadLocal存在的問題

先使用普通的ThreadLocal類做一個測試,在main方法中通過new Thread方法來開啓子線程,在子線程中打印父線程中set的ThreadLocal值。

public static void main(String[] args) throws Exception {

        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("初始化的值能繼承嗎?");

        System.out.println("父線程的ThreadLocal值:" + threadLocal.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子線程到了");
                System.out.println("子線程的ThreadLocal值:" + threadLocal.get());
            }
        }).start();
    }

子線程打印的結果爲null,說明父線程的ThreadLocal值在子線程中並未得到傳遞,而是中斷了。 

二、InheritableThreadLocal 

1. InheritableThreadLocal源碼分析

由於ThreadLocal父線程無法傳遞本地變量到子線程中,於是JDK引入了InheritableThreadLocal類,該類的首部描述:該類繼承了ThreadLocal類,用以實現父線程到子線程的值繼承;創建子線程時,子線程將接收父線程具有值的所有可繼承線程局部變量的初始值。通常子線程的值與父線程相同;子線程的值可以被父線程重寫的方法改變;

/**
 * This class extends <tt>ThreadLocal</tt> 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 <tt>childValue</tt>
 * method in this class.
 *
 * <p>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.
 *
 * @author  Josh Bloch and Doug Lea
 * @see     ThreadLocal
 * @since   1.2
 */

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

共有如下三個方法,沒有什麼特殊的地方: 

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    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);
    }
}

我們以最簡單的創建線程爲例

Thread thread = new Thread(...)

2. Thread類源碼分析

通過查看Thread類源碼,其有兩個全局變量

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Thread構造器,會調用init方法

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

再看init方法,對inheritableThreadLocal變量進行了初始化,將當前線程的inheritableThreadLocal賦值給子線程的inheritableThreadLocal,完成了父線程到子線程的變量傳遞。

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ...
        Thread parent = currentThread();
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        ...
    }

 3. 修改爲InheritableThreadLocal代碼測試

public static void main(String[] args) throws Exception {

        ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set("初始化的值能繼承嗎?");

        System.out.println("父線程的ThreadLocal值:" + threadLocal.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子線程到了");
                System.out.println("子線程的ThreadLocal值:" + threadLocal.get());
            }
        }).start();
    }

運行結果,可看到在new Thread創建子線程的場景中,子線程已經成功打印了父線程的ThreadLocal值。

4. InheritableThreadLocal在其他場景存在的問題

但是InheritableThreadLocal仍然存在一個問題,上述我們解決了在創建Thread時的傳遞,其傳遞時機是在創建線程時;但在線程池複用的情景中,沒有創建Thread的觸發條件。

舉一個失敗案例:創建一個固定大小爲2的線程池,進行五次子線程執行,每次給父線程的threadLocal重新賦值,由於線程池大小爲2,因此只有前兩次觸發初始化線程池,後面三次均爲線程複用,理論上來說,子線程可繼承的父線程變量到i=1截止。

public static void main(String[] args) throws Exception {
        ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

        ExecutorService threadPool = Executors.newFixedThreadPool(2);// 創建一個固定大小爲2的線程池
        for (int i = 0; i < 5; i++) {
            threadLocal.set("初始化的值能繼承嗎?" + i);
            System.out.println("父線程的ThreadLocal值:" + threadLocal.get());
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("子線程到了");
                    System.out.println("=========子線程的ThreadLocal值:" + threadLocal.get());
                }
            });
        }
    }

運行結果如我所料,子線程打印的值只有0和1

三、TransmittableThreadLocal(阿里開源)

使用線程池時,ThreadLocal父線程到子線程值傳遞問題需要TransmittableThreadLocal來解決。TransmittableThreadLocal是阿里開源的專門用以解決上述問題的類,使用時需要引入maven

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.4</version>
</dependency>

TTL:https://github.com/alibaba/transmittable-thread-local#-%E9%9C%80%E6%B1%82%E5%9C%BA%E6%99%AF

1. 查看TransmittableThreadLocal源碼

其繼承了InheritableThreadLocal類,因此同樣也支持new Thread時的值傳遞;

其中set和get方法均調用了addValue方法,addValue方法使用了全局的holder變量,將this指針put入holder中,this指的就是當前線程的ThreadLocal【因爲ThreadLocal維護了線程和value的關係,不論是父線程還是子線程,都會被存入ThreadLocalMap中,因此此處的this包含了所有的父線程+子線程】

holder就是一個InheritableThreadLocal變量,該變量在子線程的執行過程中,緩存了所有線程的TransmittableThreadLocal(包括子線程+父線程),set方法緩存父線程,get方法緩存子線程(多數情況都是父線程賦值,子線程調用,所以不論是get還是set方法都要給holder存值)。

public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> {

    static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder = new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
        protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
            return new WeakHashMap();
        }

        protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
            return new WeakHashMap(parentValue);
        }
    };
    
    public final void set(T value) {
        super.set(value);
        if (null == value) {// 提供set null移除value的功能
            this.removeValue();
        } else {
            this.addValue();
        }

    }

    public final T get() {
        T value = super.get();
        if (null != value) {
            this.addValue();
        }

        return value;
    }

    void addValue() {
        if (!((Map)holder.get()).containsKey(this)) {
            ((Map)holder.get()).put(this, (Object)null);
        }
    }

}

2. TtlExecutors類使用及源碼分析

上述問題在於線程池中線程複用沒有觸發條件,而只要在子線程執行時加入複製父線程的threadLocal即可。而原生的線程池創建方法不提供此功能,因此需要單獨的線程池創建類支持,TtlExecutors,同樣也是阿里開源的。

使用場景如下:

ExecutorService threadPool= TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(poolSize));

修改剛纔的代碼,ThreadLocal和Executors修改爲阿里的 TransmittableThreadLocal和TtlExecutors:

public static void main(String[] args) throws Exception {
        ThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();

        ExecutorService threadPool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
        for (int i = 0; i < 5; i++) {
            threadLocal.set("初始化的值能繼承嗎?" + i);
            System.out.println("父線程的ThreadLocal值:" + threadLocal.get());
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("子線程到了");
                    System.out.println("=========子線程的ThreadLocal值:" + threadLocal.get());
                }
            });
        }
    }

再來看TtlExecutors類,其getTtlExecutorService方法如下,返回的是ExecutorServiceTtlWrapper類

public static ExecutorService getTtlExecutorService(ExecutorService executorService) {
        return (ExecutorService)(executorService != null && !(executorService instanceof ExecutorServiceTtlWrapper) ? new ExecutorServiceTtlWrapper(executorService) : executorService);
    }

 再去看ExecutorServiceTtlWrapper,該類中未尋找到execute方法,因此在其父類ExecutorTtlWrapper中

class ExecutorServiceTtlWrapper extends ExecutorTtlWrapper implements ExecutorService {

execute方法中,執行Runnable時,外層包裝了TtlRunnable類

class ExecutorTtlWrapper implements Executor {
    final Executor executor;

    public ExecutorTtlWrapper(Executor executor) {
        this.executor = executor;
    }

    public void execute(Runnable command) {
        this.executor.execute(TtlRunnable.get(command));
    }
}

看下TtlRunnable類的run方法(多線程的執行處,到達run方法時,子線程內部邏輯已經執行完畢,包括子線程的ThreadLocal的get或者set),發現以下4句是關鍵語句,正常run方法執行邏輯應該只有3,TtlRunnable的run實際上是在正常run的前後包裝了邏輯:

  1. 一進入TtlRunnable就將當前的所有TransmittableThreadLocal存儲在AtomicReference(copied)中
  2. 在子線程執行前,將copied(即1獲取的)備份在backup中,copied中不存在的,holder中存在的,是剛剛加入的子線程threadLocal,把copied的全部remove掉,只留下該子線程的。
  3. 執行子線程(runnable的類是Runnable)
  4. 在子線程執行後,把剛剛執行的threadLocal清理掉,再把backup中的set回來,完成一次神操作。
public final class TtlRunnable implements Runnable {
    private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef = new AtomicReference(TransmittableThreadLocal.copy());// 1
    
    public void run() {
        Map<TransmittableThreadLocal<?>, Object> copied = (Map)this.copiedRef.get();
        if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {
            Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);// 2

            try {
                this.runnable.run();// 3
            } finally {
                TransmittableThreadLocal.restoreBackup(backup);// 4
            }

        } else {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
    }

以下是2和4的源碼部分 

static Map<TransmittableThreadLocal<?>, Object> backupAndSetToCopied(Map<TransmittableThreadLocal<?>, Object> copied) {
        Map<TransmittableThreadLocal<?>, Object> backup = new HashMap();
        Iterator iterator = ((Map)holder.get()).entrySet().iterator();

        Entry entry;
        TransmittableThreadLocal threadLocal;

        while(iterator.hasNext()) {
            entry = (Entry)iterator.next();
            threadLocal = (TransmittableThreadLocal)entry.getKey();
            backup.put(threadLocal, threadLocal.get());// 執行備份
            if (!copied.containsKey(threadLocal)) {// 從holder中remove掉copied中不存在的
                iterator.remove();
                threadLocal.superRemove();
            }
        }

        iterator = copied.entrySet().iterator();

        while(iterator.hasNext()) {
            entry = (Entry)iterator.next();
            threadLocal = (TransmittableThreadLocal)entry.getKey();
            threadLocal.set(entry.getValue());
        }

        doExecuteCallback(true);
        return backup;
    }
static void restoreBackup(Map<TransmittableThreadLocal<?>, Object> backup) {
        doExecuteCallback(false);
        Iterator iterator = ((Map)holder.get()).entrySet().iterator();

        Entry entry;
        TransmittableThreadLocal threadLocal;
        // 把剛剛執行的那個清理掉,執行完不需要了,然後再把backup裏面的拿回來,恢復初始狀態
        while(iterator.hasNext()) {
            entry = (Entry)iterator.next();
            threadLocal = (TransmittableThreadLocal)entry.getKey();
            if (!backup.containsKey(threadLocal)) {
                iterator.remove();
                threadLocal.superRemove();
            }
        }

        iterator = backup.entrySet().iterator();

        while(iterator.hasNext()) {
            entry = (Entry)iterator.next();
            threadLocal = (TransmittableThreadLocal)entry.getKey();
            threadLocal.set(entry.getValue());
        }

    }

四、總結 

ThreadLocal

解決問題:

維護了線程和value的關係

存在問題:

無法解決父線程向子線程傳遞ThreadLocal值的問題

InheritableThreadLocal

解決問題:

JDK提供的,在通過new Thread創建子線程時,複製父線程的ThreadLocal值至子線程,觸發開關爲創建全新的子線程。

存在問題:

但線程池的線程複用機制打破了創建全新子線程的時機,導致子線程繼承到錯誤的父線程ThreadLocal(還是複用前的子線程那個值)

TransmittableThreadLocal

解決問題:

阿里開源的TTL包,配合TtlExecutors、TtlRunnable、TtlCallable實現在子線程執行時的ThreadLocal值傳遞。

解決思路:

        ThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();
        ExecutorService threadPool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
        for (int i = 0; i < 5; i++) {
            threadLocal.set("初始化的值能繼承嗎?" + i);
            System.out.println("父線程的ThreadLocal值:" + threadLocal.get());
            threadPool.execute(new Runnable() {// 初始化TtlRunnable
                @Override
                public void run() {
                    System.out.println("子線程到了");
                    System.out.println("=========子線程的ThreadLocal值:" + threadLocal.get());
                }
            });
        }

對照着剛纔的示例代碼 

  1. 在每一次調用threadLocal.set方法,都將當前的ThreadLocal存入holder的key中,值爲null;
  2. threadPool.execute方法將Runnable包裝成TtlRunnable,調用該方法時,還沒進入子線程,完成TtlRunnable的初始化,TtlRunnable的初始化會將當前所有已經產生的ThreadLocal緩存;
  3. 未完待續
發佈了43 篇原創文章 · 獲贊 54 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章