微信公衆號:Java流水賬
本號記錄國服安琪拉日常編程流水帳,歡迎後臺留言
爲啥寫ThreadLocal
背景:發現很多博客關於ThreadLocal的說明寫錯了,ThreadLocal不是維護了key爲Thread對象的Map,而是Thread對象維護了一個key爲ThreadLocal的Map。
下面截取的源碼說明了這個問題,如果覺得晦澀,可以先看後面的實例,再回過頭來看源碼。
//類Thread
public class Thread implements Runnable {
/***Thread類內部維護了一個ThreadLocalMap變量***/
ThreadLocal.ThreadLocalMap threadLocals = null;
}
//類ThreadLocal
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread(); //獲取當前線程對象
ThreadLocalMap map = getMap(t); //拿到線程私有的ThreadLocalMap
if (map != null)
map.set(this, value); //ThreadLocal對象爲key
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //獲取Thread對象私有ThreadLocalMap對象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//拿到節點
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //設置初始值
}
//設置Thread對象的ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //hashcode & 運算去除高位 等同於取餘
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
一般像網絡請求,使用線程池技術的,Tomcat/Jetty等容器,一個請求一個線程來處理的,如果有一些信息是線程共享的,有二種方式保證線程安全:
- 一種就是常規的加鎖,當然也包括cas這種;
- 一種就是使用ThreadLocal做線程隔離,線程訪問的是私有的變量,修改的也是線程對象Map自己的那份。
這種思想是典型的用空間換取降低線程安全風險和加鎖耗時的做法。
看一個多線程處理請求的例子,這張圖可以結合源碼看:
Request對象是Handler的成員變量,多線程訪問有線程安全問題,使用ThredLocal對Request對象做線程隔離,可以讓多線程執行handle()線程安全,再次說明:線程內部Map key爲ThreadLocal對象,value爲Request對象,因此實際上是每個線程有自己的線程局部變量,省去了鎖的開銷。
下面是一段簡單用法,只是爲了演示寫的。
public static void main(String[] args) {
ThreadLocal<Person> threadLocal = new ThreadLocal<>(); //每個線程局部變量都需要創建一個ThreadLocal對象
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
AtomicInteger atomicInteger = new AtomicInteger(1000);
ExecutorService executorService = Executors.newCachedThreadPool();
//for(int i 0->10)
executorService.submit(() -> {
Person person = new Person();
person.setIdcard(atomicInteger.getAndIncrement());
person.setName(Thread.currentThread().getName());
threadLocal.set(person);
threadLocal2.set("haha");
System.out.println(threadLocal.get());
System.out.println(threadLocal2.get());
System.out.println();
threadLocal.remove();
});
executorService.shutdown();
}
我的實際應用
舉一個我的項目中使用場景:
背景:上一個項目在優化整體代碼,把項目我負責的主體的實現方式做了調整,ThreadLocal在其中扮演了非常重要的角色。需求(使用場景):客戶端有一個請求過來,Java程序根據請求報文的參數決定調用某個服務提供數據。相信大家都會有類似的需求,拿我們業務來說,請求報文如下:
{ “appid”: “88888888”, “userid”: 80, “datatype”: “***”, “data”:"{“dataids”:[146,147,148]}" }
一個用戶請求過來,需要根據報文中的datatype字段決定調取不同的數據提供服務。大體如下圖所示:
我在基類定義了一個ThreadLocal 對象來隔離PullDataNotification(封裝請求信息)這個類成員變量,因爲之前項目由於線程安全問題,每一個請求都是new 一個全新的Provider 處理來規避這個問題,看Cat上GC新生代很頻繁,爲了性能提升使用ThreadLocal 只創建一個對象,降低對象的頻繁創建和銷燬。
public abstract class CommonCreditProvider<E extends CreditArgument> implements IDataProvider{
//基類定義ThreadLocal對象,如果有多個變量做線程隔離可以初始化多個對象
protected ThreadLocal<PullDataNotification> notificationThreadLocal = new ThreadLocal<>()
//主要處理函數 實際處理邏輯在process,由子類完成
@Override
PullDataResult getData(PullDataNotification notification) throws Exception {
PullDataResult pullDataResult = null
try{
notificationThreadLocal.set(notification)
getThreeElements()
E creditArgument = initArguments()
pullDataResult = process(creditArgument)
}catch (Exception ex){
throw ex
}finally{
//*****使用完手動remove******
notificationThreadLocal.remove()
}
return pullDataResult
}
}
重點:關注一個細節,我finally 代碼塊手動remove把線程變量從Thread對象的ThreadLocalMap中移除,因爲如果不這麼做可能有內存泄漏風險。
原因:看源碼,引用鏈,Thread -> ThreadLoalMap -> Entry[ WeakRefrence, Object] 因爲線程對象(Tomcat請求線程池等)存在,線程維護的Map的Entry數組如果不手動清除也還存在,根據JVM GC的根路徑定位,那數組中Entry的key,value是變量都在鏈上,所以會有內容泄漏的風險,所以要在使用完之後手動調用remove()函數清除。
後記
網上的很多文章,講ThreadLocal中的ThreadLocalMap存放的key是線程對象,value是設置的線程局部變量,乍一看覺得挺有道理的,也算實現了線程數據隔離。但是仔細想不合理,如果有多個變量都要線程隔離,key又是線程對象,那key不夠用啊!看源碼,發現網上博客很多寫的是錯的,所以還是以後遇到有疑問的多想想What? How?Why?這東西是什麼?怎麼用? 爲什麼這麼實現?合理嗎?
這裏沒有敷衍的複製粘貼,博眼球的面試資料分享,有的只是儘可能清晰的講清個人開發中遇到的一個個問題和總結。歡迎大家關注Java流水賬,純粹的個人技術公衆號。