一、引言
ThreadLocal是并發(fā)場景下用來解決變量共享問題的類瓦呼,它能使原本線程間共享的對象進行線程隔離涨共,即一個對象只對一個線程可見。但由于過度設計抡四,比如使用弱引用和哈希碰撞柜蜈,導致理解難度大、使用成本高指巡,反而成為故障高發(fā)點淑履,容易出現(xiàn)內(nèi)存泄漏、臟數(shù)據(jù)藻雪、共享對象更新等問題秘噪。
本文從Java引用類型、ThreadLocal源碼解析勉耀、ThreadLocal使用注意事項三個方面展開指煎。首先來看一段ThreadLocal的使用示例:
// ThreadLocal使用示例
public class ThreadLocalUtil {
private static final ThreadLocal<Integer> testThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(4);
for (int i = 0; i < 4; i ++) {
new Thread(new TestThread(barrier)).start();
}
}
static class TestThread implements Runnable{
private CyclicBarrier barrier;
public TestThread(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
barrier.await();
for (int i = 0; i < 100; i++) {
Integer value = testThreadLocal.get();
if (value == null) {
value = 0;
}
Integer sum = value + i;
testThreadLocal.set(sum);
}
System.out.println(Thread.currentThread().getName() + " sum is " + testThreadLocal.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
從運行結(jié)果可以看出,每個線程的求和結(jié)果都是4950便斥,線程之間沒有相互影響至壤。
二、引用類型
前面有篇文章介紹過JVM垃圾回收的可達性分析機制枢纠。對象在堆上創(chuàng)建之后所持有的引用是一種變量類型像街,引用的可達性是判斷能否被垃圾回收的基本條件。我們可以把引用分為強引用晋渺、軟引用镰绎、弱引用和虛引用四類。
- 強引用(Strong Reference):最為常見些举。如
Object object = new Object();
這樣的變量聲明和定義就會產(chǎn)生對該對象的強引用跟狱。只要對象有強引用指向,并且GC Roots可達户魏,那么Java內(nèi)存回收時驶臊,即使瀕臨內(nèi)存耗盡挪挤,也不會回收該對象。 - 軟引用(Soft Reference):引用力度弱于強引用关翎,用于非必須對象上扛门。在即將OOM之前,垃圾回收器會把這些軟引用指向的對象加入回收范圍纵寝,以獲得更多的內(nèi)存空間论寨。軟引用主要用來緩存中間計算結(jié)果及不需要實時保存的用戶行為等。
- 弱引用(Weak Reference):引用強度較前兩者更弱爽茴,也是用來描述非必需對象的葬凳。如果弱引用指向的對象只存在弱引用這條線路,則在下一次YGC時會被回收室奏。由于YGC時間的不確定性火焰,弱引用何時被回收也具有不確定性。調(diào)用 WeakReference. get()可能返回null胧沫,要注意空指針異常昌简。
- 虛引用(Phantom Reference):是極弱的一種引用關(guān)系,定義完成后就無法通過該引用獲取指向的對象绒怨。為一個對象設置虛引用的唯一目的就是希望能在這個對象被回收時收到一個系統(tǒng)通知纯赎。虛引用必須與引用隊列聯(lián)合使用,當垃圾回收時南蹂,如果發(fā)現(xiàn)存在虛引用犬金,就會在回收對象內(nèi)存前,把這個虛引用加入與之關(guān)聯(lián)的引用隊列中六剥。
除強引用外佑附,其他三種引用可以減少對象在生命周期中所占用的內(nèi)存大小。使用這些引用仗考,需要注意強引用劫持音同、內(nèi)存泄漏等問題。
三秃嗜、ThreadLocal原理
自己實現(xiàn)ThreadLocal权均?
如果讓我們自己實現(xiàn)一個ThreadLocal,可能最直接的想法就是維護一個map锅锨,將線程作為key來獲取該線程的value叽赊,例如下面這段代碼:
public class MyThreadLocal<T> {
private Map<Thread, T> keyValueMap = new WeakHashMap<>();
public synchronized void set(T value) {
Thread thread = Thread.currentThread();
keyValueMap.put(thread, value);
}
public synchronized T get() {
Thread thread = Thread.currentThread();
return keyValueMap.get(thread);
}
public synchronized void remove() {
Thread thread = Thread.currentThread();
keyValueMap.remove(thread);
}
}
這段代碼最大的問題就是synchronized的使用會導致并發(fā)性能較差。那么必搞,jdk中的ThreadLocal是如何實現(xiàn)的呢必指?
ThreadLocal類源碼分析
set方法
先來看set方法,源碼如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到恕洲,set方法是通過一個ThreadLocalMap對象來set值塔橡,而ThreadLocalMap雖然是ThreadLocal的靜態(tài)內(nèi)部類梅割,卻是Thread類中的成員變量。這跟我們設想的完全不一樣葛家!
get方法
再來看get方法户辞,源碼:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
這里get方法也是將操作委托給了ThreadLocalMap,通過最終獲得ThreadLocalMap.Entry來獲取最終的值癞谒。
remove方法
最后來看remove方法底燎,同樣是交給ThreadLocalMap來處理
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap源碼分析
上文說過,ThreadLocalMap是線程的私有成員變量弹砚。我理解這樣做是為了避免多線程競爭双仍,因為放在Thread對象中就相當于線程私有了,處理的時候不需要加鎖桌吃。由于ThreadLocal本身的設計就是變量不與其他線程共享殊校,不需要其他線程訪問本對象的變量,放在Thread對象中不會有問題读存。
Entry數(shù)據(jù)結(jié)構(gòu)
ThreadLocalMap維護了一個Entry類型的數(shù)據(jù)結(jié)構(gòu):
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
Entry是一個以ThreadLocal為key,Object為value的鍵值對呕屎。需要注意的是让簿,threadLocal是弱引用,即使線程正在執(zhí)行中秀睛,只要ThreadLocal對象引用被置成null, Entry的Key就會自動在下一次YGC時被垃圾回收尔当。而在 ThreadLocal使用set()和get()時,又會自動地將那些 key==null 的value置為null蹂安,使value能夠被垃圾回收椭迎,避免內(nèi)存泄漏。
但是理想很豐滿田盈,現(xiàn)實很骨感畜号,ThreadLocal也因為這樣的設計導致了一些問題,下文會講到允瞧。
set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 定位Entry存放的位置
int i = key.threadLocalHashCode & (len-1);
// 處理hash沖突的情況简软,這里采用的是開放地址法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//更新
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 新建entry并插入
tab[i] = new Entry(key, value);
int sz = ++size;
// 清除臟數(shù)據(jù),擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法
private Entry getEntry(ThreadLocal<?> key) {
// 確定entry位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 命中
if (e != null && e.get() == key)
return e;
else
// 存在hash沖突述暂,繼續(xù)查找
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
//找到entry
if (k == key)
return e;
// 臟數(shù)據(jù)處理
if (k == null)
expungeStaleEntry(i);
else
//遍歷
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
remove方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//清空key
e.clear();
//清空value
expungeStaleEntry(i);
return;
}
}
}
回頭梳理
我們再回頭梳理一下ThreadLocal和Thread的類關(guān)系圖與堆棧關(guān)系圖:
上圖中的關(guān)系簡要概括:
- 1個Thread有且僅有1個ThreadLocalMap對象
- 1個Entry對象的key弱引用指向1個ThreadLocal對象
- 1個ThreadLocalMap對象存儲多給Entry對象
- 1個ThreadLocal對象可以被多個結(jié)程所共享
- ThreadLocal對象不持有Value痹升,Value由線程的Entry對象持有
四、ThreadLocal注意事項
臟數(shù)據(jù)
線程復用會產(chǎn)生臟數(shù)據(jù)畦韭。由于結(jié)程池會重用Thread對象疼蛾,那么與Thread綁定的類的靜態(tài)屬性ThreadLocal變量也會被重用。如果在實現(xiàn)的線程run()方法體中不顯式地調(diào)用remove() 清理與線程相關(guān)的ThreadLocal信息艺配,那么倘若下一個結(jié)程不調(diào)用set() 設置初始值察郁,就可能get() 到重用的線程信息衍慎,包括 ThreadLocal所關(guān)聯(lián)的線程對象的value值。
內(nèi)存泄漏
通常我們會使用使用static關(guān)鍵字來修飾ThreadLocal(這也是在源碼注釋中所推薦的)绳锅。在此場景下西饵,其生命周期就不會隨著線程結(jié)束而結(jié)束,寄希望于ThreadLocal對象失去引用后鳞芙,觸發(fā)弱引用機制來回收Entry的Value就不現(xiàn)實了眷柔。如果不進行remove() 操作,那么這個線程執(zhí)行完成后原朝,通過ThreadLocal對象持有的對象是不會被釋放的驯嘱。
以上兩個問題的解決辦法很簡單,就是在每次用完ThreadLocal時喳坠, 必須要及時調(diào)用 remove()方法清理鞠评。
父子線程共享線程變量
很多場景下通過ThreadLocal來透傳全局上下文,會發(fā)現(xiàn)子線程的value和主線程不一致壕鹉。比如用ThreadLocal來存儲監(jiān)控系統(tǒng)的某個標記位剃幌,暫且命名為traceId。某次請求下所有的traceld都是一致的晾浴,以獲得可以統(tǒng)一解析的日志文件负乡。但在實際開發(fā)過程中,發(fā)現(xiàn)子線程里的traceld為null脊凰,跟主線程的并不一致抖棘。這就需要使用InheritableThreadLocal來解決父子線程之間共享線程變量的問題,使整個連接過程中的traceId一致狸涌。
參考資料
- ThreadLocal源碼
- 《碼出高效》
- 慕珵:《ThreadLocal源碼學習》