前言
我們都知道ThreadLocal
用于為每個線程存儲自己的變量值衅斩,起到線程間隔離的作用船老,那么它到底是怎么運行的呢琉苇,讓我們通過一段demo來進行一下源碼分析匕垫。
public static void main(String[] args) {
ThreadLocal<Integer> sThreadLocal = new ThreadLocal<Integer>();
new Thread(()->{sThreadLocal.set(1);System.out.println("線程1的threadlocal值:"+sThreadLocal.get());}).start();
new Thread(()->{sThreadLocal.set(2);System.out.println("線程2的threadlocal值:"+sThreadLocal.get());}).start();
}
輸出結果:
線程1的threadlocal值:1
線程2的threadlocal值:2
源碼解析
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);
}
這里調用了getMap(t)
方法,來看一下
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到返回了當前線程的threadLocals
屬性除破,當該屬性不為空時調用其對應的set
方法牧氮,否則調用createMap
方法進行初始化,首先來看一下createMap
方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
這里主要做的事情是初始化當前線程的threadLocals
瑰枫,來看一下構造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
這里首先創(chuàng)建了一個Entry
類型的數(shù)組踱葛,數(shù)組大小為INITIAL_CAPACITY
的值16,Entry
是ThreadLocal
的一個內(nèi)部類光坝,定義為
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
該類繼承了WeakReference
尸诽,因此很明顯是一種弱引用的方式,這里其實存在一個潛在的內(nèi)存泄漏問題盯另,那就是key因為弱引用的關系回收了性含,但該Entry
對象由于仍可能被ThreadLocalMap
對象強引用而無法釋放,這樣該Entry
就變成了一個“臟對象”鸳惯,為此代碼里在其他地方對這個問題進行了優(yōu)化商蕴,后面會講到。
i
是數(shù)組中的下標芝发,通過當前線程的threadLocalHashCode
計算得來绪商,而threadLocalHashCode
的計算過程如下:
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
這里的nextHashCode
定義如下
private static AtomicInteger nextHashCode =
new AtomicInteger();
所以threadLocalHashCode
實質是一個以指定步長進行累加的累加器,該步長能較好的將連續(xù)的線程ID散列到2的冪次方的數(shù)組中后德。另外需要說明的是部宿,傳入的Entry
的key值是當前ThreadLocal
對象,也就是說這個ThreadLocal
對象是被弱引用的對象瓢湃,如果沒有別的地方對其進行了強引用理张,一旦觸發(fā)gc該對象就會被回收。
看完createMap
方法初始化map
后绵患,來看set
方法
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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)]) {//遍歷Entry不為空的節(jié)點
ThreadLocal<?> k = e.get();
if (k == key) { //若該Entry的key為當前的ThreadLocal對象
e.value = value;
return;
}
if (k == null) { //若該ThreadLocal對象已被回收
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);//遍歷到Entry空的節(jié)點則創(chuàng)建
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
由上述代碼看到雾叭,這里主要做的是在一個for循環(huán)中遍歷尋找Entry
不為空的節(jié)點,一旦獲取到就填入新的Entry
值落蝙,更新數(shù)組size
并根據(jù)閾值判斷是否執(zhí)行rehash()
方法更新數(shù)組织狐。
而當遍歷到的Entry
為非空節(jié)點時,會有以下操作:若該Entry
的key為當前的ThreadLocal
對象時筏勒,直接賦值value移迫;若當獲取到的Entry
為臟對象時,會調用replaceStaleEntry(key, value, i)
方法進行清理管行。
清理方法
這里有幾個方法值得我們具體看一下厨埋,首先是cleanSomeSlots(i, sz)
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);//找到臟entry并清除掉
}
} while ( (n >>>= 1) != 0);//通過n控制循環(huán)次數(shù)
return removed;
}
該方法用來遍歷清除臟Entry
,一旦遍歷過程中發(fā)現(xiàn)了臟Entry
捐顷,則會調用expungeStaleEntry(i)
方法清除掉荡陷,并且重置n增加遍歷次數(shù)雨效。那么expungeStaleEntry(i)
做了什么呢
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
可以看到清除臟Entry
的方式其實很簡單,就是將該Entry
位置設為null废赞,這樣一來失去了強引用的臟Entry
就會被gc回收徽龟。另外可以看到的是,expungeStaleEntry(i)
方法清除了i位置的臟Entry
后唉地,并不會停下据悔,而是會繼續(xù)遍歷下一個位置清除臟Entry
。
接著看一下replaceStaleEntry(key, value, i)
方法
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;//向前找到第一個臟Entry
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//如果在查找過程中還未發(fā)現(xiàn)臟Entry渣蜗,那么就以當前位置作為清除的起點
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果向前未搜索到臟Entry屠尊,而在查找過程遇到臟Entry的話,后面就以此時這個位置作為起點執(zhí)行清除
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 沒有發(fā)現(xiàn)對應的key耕拷,則在該臟位置創(chuàng)建新Entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//清除剩余臟Entry
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
該方法首先前向搜尋臟Entry記錄為slotToExpunge
,接著從staleSlot
位置開始后向搜索托享,如果在查找過程中未發(fā)現(xiàn)臟Entry骚烧,且存在當前的key,那么賦值value闰围,并且以當前位置staleSlot
作為清除的起點赃绊;若for循環(huán)結束仍未找到對應的key,則在staleSlot
位置創(chuàng)建新的Entry節(jié)點羡榴,并從slotToExpunge
位置開始清除剩余的臟Entry碧查。
get方法
看完了ThreadLocal
的set方法,接著來看看其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();
}
這里可以看到校仑,首先通過getMap
方法獲取當前線程的threadLocals
忠售,如果該map不為空,以當前ThreadLocal
對象做為key取出對應的Entry
得到value值迄沫。若沒有順利取得value值稻扬,則會執(zhí)行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;
}
initialValue()
方法為value設置了null值泰佳,通過當前線程獲取threadLocals
,若map存在則調用set方法尘吗,否則調用createMap
方法創(chuàng)建threadLocals
逝她。
總結和思考
從以上分析可以了解到,Thread
對象持有自己的ThreadLocalMap
對象睬捶,該對象實質為一個Entry
數(shù)組黔宛,每個Entry
是一個鍵值對,key是當前的ThreadLocal
對象侧戴,并且對該ThreadLocal
對象使用的是弱引用宁昭。這里存在兩個問題:
- 為什么采用這種引用結構跌宛;
- 這里是否存在內(nèi)存泄漏問題。
對于問題1积仗,由于ThreadLocal
的生命周期普遍長于Thread
疆拘,因此當Thread
生命周期結束以后,即使ThreadLocal
仍存在寂曹,但由于弱引用的關系哎迄,ThreadLocalMap
就可以被釋放了。
低于問題2隆圆,當ThreadLocal
提前于Thread
結束生命周期漱挚,比如線程池這種Thread
長期不結束的情況,此時ThreadLocal
對象僅有來自ThreadLocalMap
中Entry
的弱引用渺氧,因此該ThreadLocal
對象時可以被回收掉的旨涝,那么接下來就會出現(xiàn)對應的Entry
中key被置為null的情況,那么這個Entry
就再也不可能被調用到侣背,就發(fā)生了內(nèi)存泄漏白华。為了處理這種情況,在源碼的set方法中我們看到了大量的臟Entry
清理策略贩耐,另外其實在remove方法中也有類似的清理策略弧腥,我們也在使用完ThreadLocal
后采用手動調用remove方法的方式來避免內(nèi)存泄漏的情況。