加強版的ThreadLocal-TransmittableThreadLocal

1、前言

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

2、TransmittableThreadLocal

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

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修改代碼以下:spa

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,可知子線程訪問到了父線程的線程變量線程

3、總結

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

file

相關文章
相關標籤/搜索