增強版的ThreadLocal-TransmittableThreadLocal

一、前言

ThreadLocal是JDK裏面提供的一個thread-local(線程局部)的變量,當一個變量被聲明爲ThreadLocal時候,每個線程會持有該變量的一個獨有副本;但是ThreadLocal不支持繼承性,雖然JDK裏面提供了InheritableThreadLocal來解決繼承性問題,但是其也是不徹底的,本節我們談談增強的TransmittableThreadLocal,其可以很好解決線程池情況下繼承問題。

二、TransmittableThreadLocal

前面說了,當一個變量被聲明爲ThreadLocal時候,每個線程會持有該變量的一個獨有副本,比如下面例子:

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

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

        new Thread(() -> {
            try {
                // 設置本線程變量
                parent.set(Thread.currentThread().getName()   "hello,jiaduo");

                // dosomething
                Thread.sleep(3000);

                // 使用線程變量
                System.out.println(Thread.currentThread().getName()   ":"   parent.get());

                // 清除
                parent.remove();
                
                // do other thing
                //.....

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "thread-1").start();

        new Thread(() -> {
            try {
                // 設置本線程變量
                parent.set(Thread.currentThread().getName()   "hello,jiaduo");

                // dosomething
                Thread.sleep(3000);

                // 使用線程變量
                System.out.println(Thread.currentThread().getName()   ":"   parent.get());

                // 清除
                parent.remove();
                
                // do other thing
                //.....
                
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "thread-2").start();
}

如上代碼線程1和線程2各自持有parent變量中的副本,其相互之間併發訪問自己的副本變量,不會存在線程安全問題。

但是ThreadLocal不支持繼承性:

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

        ThreadLocal<String> parent = new ThreadLocal<String>();
        parent.set(Thread.currentThread().getName()   "hello,jiaduo");

        new Thread(() -> {

            try {
                
                // 使用線程變量
                System.out.println(Thread.currentThread().getName()   ":"   parent.get());

                // do other thing
                // .....

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "child-thread").start();
}

如上代碼main線程內設置了線程變量,然後在main線程內開啓了子線程child-thread,然後在子線程內訪問了線程變量,運行會輸出:child-thread:null;也就是子線程訪問不了父線程設置的線程變量;JDK中InheritableThreadLocal可以解決這個問題:

        InheritableThreadLocal<String> parent = new InheritableThreadLocal<String>();
        parent.set(Thread.currentThread().getName()   "hello,jiaduo");

        new Thread(() -> {

            try {
                
                // 使用線程變量
                System.out.println(Thread.currentThread().getName()   ":"   parent.get());

                // do other thing
                // .....

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "child-thread").start();

運行代碼會輸出:child-thread:mainhello,jiaduo,可知InheritableThreadLocal支持繼承性。但是InheritableThreadLocal的繼承性是在new Thread創建子線程時候在構造函數內把父線程內線程變量拷貝到子線程內部的(可以參考《Java併發編程之美》一書),而線上環境我們很少親自new線程,而是使用線程池來達到線程複用,線上環境一般是把異步任務投遞到線程池內執行;所以父線程向線程池內投遞任務時候,可能線程池內線程已經創建完畢了,所以InheritableThreadLocal就起不到作用了,例如下面例子:

    // 0.創建線程池
    private static final ThreadPoolExecutor bizPoolExecutor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(1));

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

        // 1 創建線程變量
        InheritableThreadLocal<String> parent = new InheritableThreadLocal<String>();

        // 2 投遞三個任務
        for (int i = 0; i < 3;   i) {
            bizPoolExecutor.execute(() -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });

        }

        // 3休眠4s
        Thread.sleep(4000);

        // 4.設置線程變量
        parent.set("value-set-in-parent");

        // 5. 提交任務到線程池
        bizPoolExecutor.execute(() -> {
            try {
                // 5.1訪問線程變量
                System.out.println("parent:"   parent.get());
            } catch (Exception e) {
                e.printStackTrace();
            }

        });
}

如上代碼2向線程池投遞3任務,這時候線程池內2個核心線程會被創建,並且隊列裏面有1個元素。然後代碼3休眠4s,旨在讓線程池避免飽和執行拒絕策略,然後代碼4設置線程變量,代碼5提交任務到線程池。運行輸出:parent:null,可知子線程內訪問不到父線程設置變量。

下面我們使用TransmittableThreadLocal修改代碼如下:

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

        // 1 創建線程變量
        TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();

        // 2 投遞三個任務
        for (int i = 0; i < 3;   i) {
            bizPoolExecutor.execute(() -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });

        }

        // 3休眠4s
        Thread.sleep(4000);

        // 4.設置線程變量
        parent.set("value-set-in-parent");

        // 5. 提交任務到線程池
        Runnable task = () -> {
            try {
                // 5.1訪問線程變量
                System.out.println("parent:"   parent.get());
            } catch (Exception e) {
                e.printStackTrace();
            }

        };
        
        // 額外的處理,生成修飾了的對象ttlRunnable
        Runnable ttlRunnable = TtlRunnable.get(task);
        
        bizPoolExecutor.execute(ttlRunnable);
}

如上代碼5我們把具體任務使用TtlRunnable.get(task)包裝了下,然後在提交到線程池,運行代碼,輸出:parent:value-set-in-parent,可知子線程訪問到了父線程的線程變量

三、總結

TransmittableThreadLocal完美解決了線程變量繼承問題,其是淘寶技術部 哲良開源的一個庫,github地址爲:https://github.com/alibaba/transmittable-thread-local,後面我們會探討其內部實現原理,敬請期待。

file

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章