錯誤使用TransmittableThreadLocal讓ThreadLocal變量變成線程共享

前言

ThreadLocal能夠在單個線程中傳遞參數(shù),使用可以用在系統(tǒng)參數(shù)的傳遞或者在鏈路跟蹤中傳遞trace相關(guān)信息,需要說明的是單單使用ThreadLocal是不會出現(xiàn)ThreadLocal值線程共享的,但僅僅使用ThreadLocal還不夠,如果代碼中有使用異步,ThreadLocal就無能為力了,這時可以使用JDK自帶的InheritableThreadLocal,這次ThreadLocal變量線程共享,就是因為使用了InheritableThreadLocal。

我們的項目使用springboot構(gòu)建,想使用ThreadLocal來完成透傳系統(tǒng)參數(shù),這樣所有接口和方法都不要顯式的傳遞次參數(shù),剛剛說到ThreadLocal無法解決異步傳遞問題,InheritableThreadLocal也只能解決新建線程的情況,無法在線程池的場景中使用,具體原因下次分析,經(jīng)過調(diào)研我們使用阿里開源的TransmittableThreadLocal

示例

我們先來看這個例子:

public class TransmittableThreadLocalUtil {

    private static Logger logger = LoggerFactory.getLogger(TransmittableThreadLocalUtil.class);

    private static final TransmittableThreadLocal<Map<String, Object>> threadLocal = new TransmittableThreadLocal() {
        @Override
        protected Object initialValue() {
            return new HashMap(4);
        }
    };
    
    public static <T> T get(String key) {
        logger.info("get {}",key);
        Map map = (Map)threadLocal.get();
        return (T)map.get(key);
    }

    public static void set(String key, Object value) {
        Map map = (Map)threadLocal.get();
        map.put(key, value);
    }

    public static <T> T remove(String key) {
        Map map = (Map)threadLocal.get();
        return (T)map.remove(key);
    }

使用阿里的TransmittableThreadLocal封裝了一個工具類,初始化一個map,有g(shù)et、set、remove方法,其他方法省略。

    @GetMapping("/testLocal2")
    public void test2(){
        logger.info("testLocal2 get "+ TransmittableThreadLocalUtil.get("test"));
    }

    @GetMapping("/testLocal3")
    public void test3(){
        TransmittableThreadLocalUtil.set("test","200");
    }

兩個接口,一個設(shè)置test為200,另一個方法獲取,springboot默認使用tomcat作為容器,我們知道tomcat在處理請求時使用了線程池,理論上2個接口的調(diào)用不會是同一個線程,我們看log輸出可以確認:

2020-08-09 22:59:36.765  INFO 86354 --- [nio-8080-exec-1] c.j.controller.ThreadLocalController     : testLocal3 test set 200
2020-08-09 22:59:37.755  INFO 86354 --- [nio-8080-exec-2] c.j.controller.ThreadLocalController     : testLocal2 get 200

先調(diào)用了/testLocal3接口線程名稱是nio-8080-exec-2,再調(diào)用了/testLocal2,線程名稱是nio-8080-exec-1,是兩個線程

但是通過log可以看到第二個線程居然取到了第一個線程設(shè)置的ThreadLocal變量,這是怎么回事?ThreadLocal不是線程隔離的嗎?

上面只是用一個小例子幫助大家理解,實際中我們使用攔截器在方法調(diào)用前獲取系統(tǒng)參數(shù)放入ThreadLocal中,在調(diào)用結(jié)束時清理ThreadLocal變量,但因為同樣有上面例子中的問題,我們的一此請求沒結(jié)束時可能ThreadLocal中的值就被別的線程清理的,導(dǎo)致業(yè)務(wù)異常。

原因

這到底是什么原因?qū)е碌哪?,在翻看了TransmittableThreadLocal的源碼以及項目中的使用方式后找到了原因。

TransmittableThreadLocal繼承自jdk的InheritableThreadLocal,而InheritableThreadLocal的原理是父線程創(chuàng)建子線程時會將父線程的ThreadLocalMap復(fù)制到子線程中,這個復(fù)制是引用復(fù)制,也就是說子線程可以修改父線程ThreadLocal中的變量,但這也解釋不了為什么ThreadLoal會線程共享,tomcat線程池中的線程都是子線程啊,那只可能是父線程出現(xiàn)了問題,再看一下項目代碼,找到了原因。

因為在service的靜態(tài)變量中使用了TransmittableThreadLocalUtil的get方法初始化了那個map,而springboot在加載service時使用的是main主線程,是所有線程的父線程,導(dǎo)致所有的子線程通過TransmittableThreadLocal獲取的Map是同一個引用,當然是線程共享了!

例子中的service示例:

@Service
public class TestService {
    //靜態(tài)變量的調(diào)用
    private String value = TransmittableThreadLocalUtil.get("test");

    public void testMethod(){

    }

}

get方法中有l(wèi)og,再看一眼啟動log

2020-08-09 22:59:24.104  INFO 86354 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2164 ms
2020-08-09 22:59:24.202  INFO 86354 --- [           main] c.j.util.TransmittableThreadLocalUtil    : get test
2020-08-09 22:59:24.808  INFO 86354 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 

日志第二行,main線程調(diào)用了TransmittableThreadLocalUtil,確定了推斷。

其中一種解決方式是在TransmittableThreadLocalUtil的初始化TransmittableThreadLocal對象時復(fù)寫childValue方法

private static final TransmittableThreadLocal<Map<String, Object>> threadLocal = new TransmittableThreadLocal() {
        @Override
        protected Object initialValue() {
            return new HashMap(4);
        }
            @Override
        protected Object childValue(Object parentValue) {
            if(parentValue instanceof Map){
                return new HashMap<>((Map)parentValue);
            }
            return parentValue;
        }
    };

總結(jié)

1、ThreadLocal不會出現(xiàn)線程間共享的情況。

2、InheritableThreadLocal的原理是新建子線程時將父線程ThreadLocal復(fù)制到子線程中,是引用復(fù)制,會導(dǎo)致父子線程都能操作那個引用。

3、問題不是TransmittableThreadLocal引起的,是因為錯誤使用了InheritableThreadLocal,TransmittableThreadLocal只是繼承自InheritableThreadLocal。

為什么復(fù)寫childValue方法就可以解決呢,ThreadLocal有沒有別的坑,InheritableThreadLocal以及ThreadLocal中相關(guān)的源碼是如何處理的呢,期待下一篇吧~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容