ThreadLocal 是什麼?
ThreadLocal 是一個線程的工具類,主要用於存儲一些線程的共享變量,各個線程之間互不影響,在多線程及高併發環境下,可以實現無狀態的存儲及調用
ThreadLocal的原理
好久以前。我一直以爲ThreadLocal 可能就是一個Map,以Thead ID爲key,然後往裏面設置Value即可,但實際上JDK裏的ThreadLocal 卻沒有這樣子實現。細想,這麼實現其實會帶來兩個問題
- 多線程下,頻繁對Map 進行讀取及設值,必然要加鎖,那效率可想而知。
- 線程退出後,如果不對Map進行清除,那值一直會存在,隨着數量的增多,必然造成內存泄露。
那JDK中ThreadLocal 是怎麼實現的呢?
我們從ThreadLocal.set(T value)方法開始進入看看
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
發現是往ThreadLocalMap 這個對象設值,那這個對象哪裏來的呢?我們接着跟蹤看看
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
原來這個ThreadLocalMap 居然是 Thread的一個局部變量,且該 變量的初始值爲空
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我們再來詳細看看ThreadLocalMap 這個類
原來ThreadLocalMap 居然是ThreadLocal下的一個靜態內部類。主要的成員是
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table; //用來保存存進去的Value
而Entry 有點類似於Map Entry 也是一個鍵值對。我們來看看 Entry的定義
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry 繼承了弱引用(爲什麼要弱引用呢,下面會解釋,先hold 住),很明顯 k 就是 ThreadLocal 對象,而Value 就是我們即將要保存的共享變量。
到了這裏,我們基本上對ThreadLocal的存儲有一個清晰的認識了,首先ThreadLocal的值不是保存在一個Map中的,他是保存在當前Thread的ThreadLocaoMap 上的Entry數組上了,其中Entry繼承弱引用,而Value 就保存在Entry的value 局部變量上。
我們來思考一下,這麼實現帶來了哪些好處。
- value 保存在了線程自己的局部變量上,就避免了多線程對同一個存儲對象頻繁進行操作的衝突,提高了效率
- 其次當線程被銷燬時,Entry.value 失去了強引用,必然也會被回收,降低了內存泄露的風險。
接下來我們繼續分析一下ThreadLocal.set(T value)方法
當要設值的時候其實調用的是ThreadLocalMap 的set(T value)方法,我們看看它的實現
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be 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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
//已經存在的key 直接更新value
e.value = value;
return;
}
if (k == null) {
//k 爲空有可能是原來存儲的被回收了,要清除,防止內存泄露
// 同時將k設置爲key value 設置爲value
replaceStaleEntry(key, value, i);
return;
}
}
//獲得了存儲下標,新生成一個Entry 存進去
tab[i] = new Entry(key, value);
int sz = ++size;
//擴容,及清除一些key null的entry
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
我們再來看看兩個蠻有意思的方法
/**
* 尋找一些過期的key ,將其替換成要設置的key 及value
*
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 往前尋找過期的slot
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 往後搜索爲空的key
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 找到了key 將value 替換
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 用於後面清除過期的slot
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 清除一些過期的slot
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
// 清除一些過期的slot 類型與二分法
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); //清除
}
} while ( (n >>>= 1) != 0); // n = n / 2
return removed;
}
以上就是對ThreadLocalMap.set(T value)的解讀。接下來我們看看ThreadLocal.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();
}
繼續看ThreadLocalMap.getEntry(ThreadLocal<> key )
private Entry getEntry(ThreadLocal<?> key) {
// 找下標
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
//找到及返回
return e;
else
//沒找到就繼續
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();
if (k == key)
return e;
if (k == null)
// 順手清除空的
expungeStaleEntry(i);
else
//繼續找下一個
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
我們再來看看ThreadLocal的remove 方法
先找到ThreadLocalMap 對象
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
*/
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 對ThreadLocal弱引用 進行claar
// 這裏爲什麼不順手把value也置爲null呢???
e.clear();
expungeStaleEntry(i);
return;
}
}
}
很顯然,清除操作只是對ThreadLocalMap的Entry對象中的弱引用clear而已。沒有對value置空。這一點在使用中是要引起注意的。
至於爲什麼不對value置爲null,我的理解是這樣的。在ThreadLocal.set(T value) 及ThreadLocal.get()兩個方法,都會觸發清除一些過期的Entry。當中就會對value 置爲null..所以這裏沒有重複操作。可見下列方法
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 要繼承WeakReference(當沒有強應用時,若發生gc弱引用一定會被回收)?
盜用一張圖
從圖中可以看出。Key 對ThreadLocal的應用是弱引用。假設該引用是強引用,那麼當我們new 的ThreadLocal對象的引用被置爲null時,堆中真正的ThreaLocal 依然不會被回收,造成內存泄露,因爲我們的key保持了對ThreadLocal的強引用。所以強引用不合理,當爲弱應用時,發生GC時ThreadLocal在堆中的東西將會被回收。
爲什麼不將vlaue 也設置爲弱引用
將value 設置爲弱引用,當該value沒有了強引用,則value會被回收。這時候我們通過ThreadLocal.get()出來的東西是空的,沒辦法滿足存儲共享變量的目的。
線程池使用ThreadLocal 有什麼需要注意的
- 最好再remove之前將value 置爲null,解除引用,因爲ThreadLocal的定時清除,很有可能會有漏網之魚
- 結束任務記得remove.