當InheritableThreadLocal遇到線程池:主線程本地變量修改後,子線程無法讀取到新值

歡迎關注本人公衆號

在這裏插入圖片描述

之前已經介紹,InheritableThreadLocal可以在子線程創建的時候,將父線程的本地變量拷貝到子線程中。
那麼問題就來了,是隻有在創建的時候才拷貝,只拷貝一次,然後就放到線程中的inheritableThreadLocals屬性緩存起來。由於使用了線程池,該線程可能會存活很久甚至一直存活,那麼inheritableThreadLocals屬性將不會看到父線程的本地變量的變化

public class InheritableThreadLocalTest1 {
    public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
    public static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主線程開啓");
        threadLocal.set(1);

        executorService.submit(() -> {
            System.out.println("子線程讀取本地變量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);

        threadLocal.set(2);

        executorService.submit(() -> {
            System.out.println("子線程讀取本地變量:" + threadLocal.get());
        });
    }
}

運行結果:

主線程開啓
子線程讀取本地變量:1
子線程讀取本地變量:1
public class InheritableThreadLocalTest1 {
    public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
    public static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主線程開啓");
        threadLocal.set(1);

        executorService.submit(() -> {
            System.out.println("子線程讀取本地變量:" + threadLocal.get());
            threadLocal.remove();
        });

        TimeUnit.SECONDS.sleep(1);

        threadLocal.set(2);

        executorService.submit(() -> {
            System.out.println("子線程讀取本地變量:" + threadLocal.get());
            threadLocal.set(3);
            System.out.println("子線程讀取本地變量:" + threadLocal.get());
            threadLocal.remove();
        });

    }
}

運行結果:

主線程開啓
子線程讀取本地變量:1
子線程讀取本地變量:null
子線程讀取本地變量:3

可以看到,由於兩次執行復用了同一個線程,所以即使父線程的本地變量發生了改變,子線程的本地變量依舊是首次創建線程時賦的值。
很多時候我們可能需要在提交任務到線程池時,線程池中的線程可以實時的讀取父線程的本地變量值到子線程中,當然可以當作參數傳遞如子線程,但是代碼不夠優雅,不夠美觀。此時可以使用alibaba的開源項目transmittable-thread-local

注意: 值傳遞

就像方法傳參都是值傳遞(如果是對象,則傳遞的是引用的拷貝)一樣,InheritableThreadLocal父子線程傳遞也是值傳遞!!

public class InheritableThreadLocalTest1 {
    public static ThreadLocal<Stu> threadLocal = new InheritableThreadLocal<>();
    public static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主線程開啓");
        threadLocal.set(new Stu("aaa",1));

        executorService.submit(() -> {
            System.out.println("子線程讀取本地變量:" + threadLocal.get());
            threadLocal.get().setAge(55);
            System.out.println("子線程讀取本地變量:" + threadLocal.get());

        });

        TimeUnit.SECONDS.sleep(1);

        System.out.println("主線程讀取本地變量:" + threadLocal.get());
        threadLocal.get().setAge(99);

        executorService.submit(() -> {
            System.out.println("子線程讀取本地變量:" + threadLocal.get());
        });
    }
}

輸出結果:

主線程開啓
子線程讀取本地變量:Stu(name=aaa, age=1)
子線程讀取本地變量:Stu(name=aaa, age=55)
主線程讀取本地變量:Stu(name=aaa, age=55)
子線程讀取本地變量:Stu(name=aaa, age=99)

爲什麼是值傳遞,還要從源碼分析,源碼中未進行拷貝,直接返回父線程對象的引用:
在這裏插入圖片描述
在這裏插入圖片描述
所以,務必關心傳遞對象的線程安全問題!!

實現線程本地變量的拷貝

上面一節講到,InheritableThreadLocal是複製的對象引用,所以主子線程其實都引用的同一個對象,存在線程安全的問題。那麼如何實現對象值的複製呢?

很簡單,只需要重寫java.lang.InheritableThreadLocal#childValue方法即可.
這裏自定義一個MyInheritableThreadLocal類,實現對象的拷貝。
我這裏使用的序列化反序列化的方式,當然也可以用其他方式。

public class MyInheritableThreadLocal<T> extends InheritableThreadLocal<T> {
    protected T childValue(T parentValue) {
        String s = JSONObject.toJSONString(parentValue);
        return (T)JSONObject.parseObject(s,parentValue.getClass());
    }
}

將上面測試類的InheritableThreadLocal改爲我自己定義的MyInheritableThreadLocal

public class InheritableThreadLocalTest1 {
    public static ThreadLocal<Stu> threadLocal = new MyInheritableThreadLocal<>();
    public static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主線程開啓");
        threadLocal.set(new Stu("aaa",1));

        executorService.submit(() -> {
            System.out.println("子線程讀取本地變量:" + threadLocal.get());
            threadLocal.get().setAge(55);
            System.out.println("子線程讀取本地變量:" + threadLocal.get());

        });

        TimeUnit.SECONDS.sleep(1);

        System.out.println("主線程讀取本地變量:" + threadLocal.get());
        threadLocal.get().setAge(99);
        System.out.println("主線程讀取本地變量:" + threadLocal.get());

        executorService.submit(() -> {
            System.out.println("子線程讀取本地變量:" + threadLocal.get());
        });
    }
}

運行結果:

主線程開啓
子線程讀取本地變量:Stu(name=aaa, age=1)
子線程讀取本地變量:Stu(name=aaa, age=55)
主線程讀取本地變量:Stu(name=aaa, age=1)
主線程讀取本地變量:Stu(name=aaa, age=99)
子線程讀取本地變量:Stu(name=aaa, age=55)

這樣,主子線程的對象纔算真正的複製過去,而不是僅僅複製了一個引用。如此就不存在線程安全的問題了

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