title: ThreadLocal小記
date: 2016-09-11 19:33:39
tags: Java
categories: Java
ThreadLocal
最近《Java并發(fā)編程實戰(zhàn)》第三章談及了對象共享的問題艇潭。對共享的可變數(shù)據(jù)最簡單粗暴的做法當然是同步,但是同步的缺點也很明顯:1. 同步會導致阻塞;2.代碼復雜可維護性降低蹋凝。針對這個問題鲁纠,書上談及到了通過線程封閉避免同步,其中的ThreadLocal類就是幫助維持線程封閉性的仙粱。
之前對ThreadLocal的認識非常簡單房交,就是把一個變量綁定到線程上彻舰。參照網(wǎng)上的例子自己也實現(xiàn)了類似功能的例子ThreadLocalVariable(https://github.com/zhanghTK/HelloJava/blob/master/src/main/java/tk/zhangh/java/thread/ThreadLocalVariable.java)伐割,但總的來說沒什么體會。今天參照別人的文章看了一下ThreadLocal的實現(xiàn)刃唤,發(fā)現(xiàn)還是有蠻多注意點的隔心。
看JDK之前想當然的以為ThreadLocal應該就是簡單對Map<Thread, Object>
做一個封裝,然而實際并沒有這么簡單尚胞。參照了網(wǎng)上一些文章的說法硬霍,早期的ThreadLocal確實是這樣實現(xiàn)的。但是這樣實現(xiàn)存在一些問題:
- 線程安全問題笼裳,如果使用線程安全的Map實現(xiàn)那么就會帶來性能問題唯卖,當有大量的線程使用ThreadLocal,伴隨著線程生命周期ThreadLocal也需要頻繁向底層的Map添加刪除數(shù)據(jù)躬柬。
- 內(nèi)存回收問題拜轨,用Thread當key,除非手動調(diào)用remove允青,否則即使線程退出了會導致:1)該Thread對象無法回收橄碾;2)該線程在所有ThreadLocal中對應的value也無法回收。
ThreadLocal實際給出了不同的實現(xiàn)方式颠锉。首先綁定到線程的變量沒有維護在ThreadLocal內(nèi)法牲,而是維護在各個Thread類實例內(nèi)——在Thread類內(nèi)使用了ThreadLocal的靜態(tài)內(nèi)部類ThreadLocalMap
實例去維護需要綁定到線程的變量。這樣原本需要維護在ThreadLocal內(nèi)的數(shù)據(jù)現(xiàn)在就分散到了各個線程內(nèi)去維護琼掠。
在Thread中ThreadLocalMap的聲明長這樣:
// 真的就只是聲明了一下拒垃,什么都沒干
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal一共只有五個非私有的方法,首先是兩個并沒有什么卵用的方法:
// 構造瓷蛙,什么都沒干
public ThreadLocal() {}
// 設置ThreadLocal的初始值悼瓮,protected很明顯是希望子類重寫
protected T initialValue() {return null}
看看其余三個方法的實現(xiàn)(JDK8)
public T get() {
Thread t = Thread.currentThread();
// 從線程里獲取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 根據(jù)ThreadLocal實例獲取Entity
// 注意key類型是ThreadLocal,不是Thread
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果獲得的map為null或者從map通過key獲取的value為空時獲取一個初始值
// setInitialValue方法里調(diào)用了initialValue方法初始化一個值
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 沒什么說的set進去
map.set(this, value);
else
// 當map不存在時速挑,使用初始值創(chuàng)建一個
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 沒什么說的谤牡,remove掉
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
// 你給我一個線程,還你一個map
return t.threadLocals;
}
ThreadLocal所有的操作都是圍繞著內(nèi)部類ThreadLocalMap的姥宝,ThreadLocal只是讓ThreadLocalMap更加容易訪問翅萤。咦,有點耳熟,沒錯外觀模式套么。
下面看一下ThreadLocal的靜態(tài)內(nèi)部類ThreadLocalMap培己,JDK文檔對它的描述主要集中在一下幾點:
- 是什么:定制的hash map用于維護本地線程變量
- 可見性:ThreadLocal之外沒有任何方法可訪問,Thread類使用它定義了私有屬性
- 特殊性:為了管理大對象胚泌、長生命周期對象省咨,ThreadLocalMap的key繼承WeakReference
前面兩點沒什么好說的,主要是第三點:key繼承了弱引用類型(關于弱引用可以先參考傳送門)玷室。
實線表示強引用零蓉,虛線表示弱引用。
簡而言之穷缤,一個對象在沒有強引用引用敌蜂,只有弱引用引用時,當GC發(fā)生這個對象就會被標記回收津肛。將ThreadLocalMap的key設置成弱引用章喉,
當不再使用ThreadLocal(沒有任何強引用引用)時,只有ThreadLocalMap的Entry對它存在弱引用身坐,這樣GC的時候這個ThreadLocal對象就可以被回收了秸脱。
key得到有效回收,但value依然存在著強引用關系部蛇√剑可能導致key被回收了,但value的引用始終存在搪花。引用關系:
Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value
即使線程不再使用 ThreadLocal遏片,但因為上面的引用關系存在仍會導致 value 無法回收,只有線程被銷毀時才能被回收撮竿。極端情況下(例如使用線程池管理線程)吮便,可能導致內(nèi)存溢出、數(shù)據(jù)錯亂幢踏。
真對這個問題ThreadLocalMap在實現(xiàn)的時候也采取了一些防護措施髓需,比如ThreadLocalMap的get(set,remove方法也類似房蝉,限于篇幅不展開了):
private Entry getEntry(ThreadLocal<?> key) {
// hash函數(shù)獲取索引位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 命中了
return e;
else
// miss了
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 遍歷table
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
// 找到了
return e;
if (k == null)
// 發(fā)現(xiàn)key為空(也就是上面描述的內(nèi)存泄漏的情況)僚匆,做刪除
expungeStaleEntry(i);
else
// 找下一個Entty位置
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
在get的過程中凡是碰到了key為null的Entry,這個Entry就會被擦除搭幻,從而避免內(nèi)存泄漏咧擂。類似的思路在set,remove方法中都有實現(xiàn)檀蹋。針對內(nèi)存泄漏問題ThreadLocal實際是需要手動觸發(fā)函數(shù)刪除key為null的Entry松申,因此對ThreadLocal維護的變量不再使用時,手動 remove 還是很有必要的。
ThreadLocal 還是有很多值得學習的地方贸桶,尤其是 ThreadLocalMap 設計的非常巧妙:
- 從設計模式的角度:
- 將線程內(nèi)使用的變量封裝在線程內(nèi)優(yōu)于全局的上線文(迪米特法則)
- ThreadLocal 屏蔽了 ThreadLocalMap 的實現(xiàn)舅逸,為客戶端提供簡單統(tǒng)一的調(diào)用方法(外觀模式)
- Java 實現(xiàn):
- ThreadLocalMap 的 key 使用弱引用可以盡快回收 ThreadLocal,對 map 的其他操作也提供了 value 回收的兜底方式皇筛,最大限度的避免了內(nèi)存溢出的可能
ThreadLocal 的最佳實踐:在業(yè)務周期完成時琉历,顯式的調(diào)用ThreadLocal 的 remove()方法。