前言
我們都知道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();
}
輸出結(jié)果:
線程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);
}
這里調(diào)用了getMap(t)方法,來看一下
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到返回了當前線程的threadLocals屬性,當該屬性不為空時調(diào)用其對應(yīng)的set方法,否則調(diào)用createMap方法進行初始化,首先來看一下createMap方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
這里主要做的事情是初始化當前線程的threadLocals,來看一下構(gòu)造方法
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因為弱引用的關(guān)系回收了,但該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實質(zhì)是一個以指定步長進行累加的累加器,該步長能較好的將連續(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為臟對象時,會調(diào)用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,則會調(diào)用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位置設(shè)為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)對應(yīng)的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)結(jié)束仍未找到對應(yīng)的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取出對應(yīng)的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設(shè)置了null值,通過當前線程獲取threadLocals,若map存在則調(diào)用set方法,否則調(diào)用createMap方法創(chuàng)建threadLocals。
總結(jié)和思考
從以上分析可以了解到,Thread對象持有自己的ThreadLocalMap對象,該對象實質(zhì)為一個Entry數(shù)組,每個Entry是一個鍵值對,key是當前的ThreadLocal對象,并且對該ThreadLocal對象使用的是弱引用。這里存在兩個問題:
- 為什么采用這種引用結(jié)構(gòu);
- 這里是否存在內(nèi)存泄漏問題。
對于問題1,由于ThreadLocal的生命周期普遍長于Thread,因此當Thread生命周期結(jié)束以后,即使ThreadLocal仍存在,但由于弱引用的關(guān)系,ThreadLocalMap就可以被釋放了。
低于問題2,當ThreadLocal提前于Thread結(jié)束生命周期,比如線程池這種Thread長期不結(jié)束的情況,此時ThreadLocal對象僅有來自ThreadLocalMap中Entry的弱引用,因此該ThreadLocal對象時可以被回收掉的,那么接下來就會出現(xiàn)對應(yīng)的Entry中key被置為null的情況,那么這個Entry就再也不可能被調(diào)用到,就發(fā)生了內(nèi)存泄漏。為了處理這種情況,在源碼的set方法中我們看到了大量的臟Entry清理策略,另外其實在remove方法中也有類似的清理策略,我們也在使用完ThreadLocal后采用手動調(diào)用remove方法的方式來避免內(nèi)存泄漏的情況。