1. 程式人生 > 其它 >三種ThreadLocal,玩轉執行緒變數儲存與傳遞

三種ThreadLocal,玩轉執行緒變數儲存與傳遞

ThreadLocal

ThreadLocal是jdk中自帶的類,用於儲存本執行緒專有的資料,從下面的程式碼大家可以很直接的看到它的作用

 private static ThreadLocal<String> sThreadLocal=new ThreadLocal<>();

    @Test
    void testThreadlocal() {
        //主執行緒
        sThreadLocal.set("這是在主執行緒中");
        System.out.println("執行緒名字:"+Thread.currentThread().getName()+"---"+sThreadLocal.get());
        //執行緒a
        new Thread(new Runnable() {
            public void run() {
                sThreadLocal.set("這是線上程a中");
                System.out.println("執行緒名字:"+Thread.currentThread().getName()+"---"+sThreadLocal.get());
            }
        },"執行緒a").start();
        //執行緒b
        new Thread(new Runnable() {
            public void run() {
                sThreadLocal.set("這是線上程b中");
                System.out.println("執行緒名字:"+Thread.currentThread().getName()+"---"+sThreadLocal.get());
            }
        },"執行緒b").start();
        
    }

執行結果:
​​​​
很明顯可以看到,sThreadLocal變數只有一份,為字串型別,但是主執行緒和ab兩個子執行緒打印出來的sThreadLocal變數值是不一樣的,這就是ThreadLocal命名的含義,建立專屬於執行緒的變數資料。
為什麼能達到這種效果呢,其實原理也很簡單,我們點進去ThreadLocal的set方法裡面看看。

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

在這個方法裡面可以看到一個叫ThreadLocalMap的變數,與Hashmap類似,我們的value就是設定到這裡面的,而再次點進去發現getMap方法是從Thread類本身去拿的,那麼為什麼ThreadLocal變數和執行緒繫結也就很好理解了,因為儲存ThreadLocal的ThreadLocalMap就是執行緒的一個變數。

    Thread.class:
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal一般配合tomcat的一個請求一個業務執行緒的模型使用,儲存類似於使用者資訊等全域性都可能用到的內容,避免變數透傳。

InheritableThreadLocal

看名字就知道,這個類是用於父子執行緒的變數傳遞,我們只需在上面測試程式碼上略作修改

  private static InheritableThreadLocal<String> sThreadLocal=new InheritableThreadLocal<>();

    @Test
    void testThreadlocal() {
        //主執行緒
        sThreadLocal.set("這是在主執行緒中");
        System.out.println("執行緒名字:"+Thread.currentThread().getName()+"---"+sThreadLocal.get());
        //執行緒a
        new Thread(new Runnable() {
            public void run() {
                System.out.println("執行緒名字:"+Thread.currentThread().getName()+"---"+sThreadLocal.get());
            }
        },"執行緒a").start();
        
    }

執行結果:

可以看到我們新建執行緒後明明沒有任何set sThreadLocal的行為,但依然能獲取到主執行緒(父執行緒)的sThreadLocal變數,這就是InheritableThreadLocal和 ThreadLocal區別所在,變數可以由父執行緒傳給子執行緒。

TransmittableThreadLocal

實際開發中InheritableThreadLocal一般很少用到,原因很簡單,正式的開發中沒人會直接new一個執行緒,這樣會導致執行緒難以管理和回收,絕大多數專案在涉及到多執行緒的時候都會使用執行緒池,但是這也帶來一個問題,執行緒池中的執行緒和業務主執行緒沒有任何從屬關係,那是不是ThreadLocal的變數只能顯式透傳過去了呢,這時候我們可以使用阿里開源的元件transmittable-thread-local 解決。

//直接使用包裝好的執行緒池即可
static ExecutorService pool =  TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(3));

如果是使用spring非同步註解@Aynsc也是同理,只需自定義非同步執行緒池為ttlExecutor即可
TransmittableThreadLocal原理也很簡單,關鍵在於自定義的TtlRunnable完成了轉存ThreadLocal動作

 @Override
    public void run() {
        final Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        //這一步就是轉存變數動作
        final Object backup = replay(captured);
        try {
            runnable.run();
        } finally {
            restore(backup);
        }
    }

//實際執行轉存方法
private static HashMap<ThreadLocal<Object>, Object> replayThreadLocalValues(@NonNull HashMap<ThreadLocal<Object>, Object> captured) {
            final HashMap<ThreadLocal<Object>, Object> backup = new HashMap<ThreadLocal<Object>, Object>();

            for (Map.Entry<ThreadLocal<Object>, Object> entry : captured.entrySet()) {
                final ThreadLocal<Object> threadLocal = entry.getKey();
                backup.put(threadLocal, threadLocal.get());

                final Object value = entry.getValue();
                if (value == threadLocalClearMark) threadLocal.remove();
                else threadLocal.set(value);
            }

            return backup;
        }