前言
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)的源碼是如何處理的呢,期待下一篇吧~