能夠找到這篇文章,說明你已開始學(xué)習(xí)Java的多線程了,也了解多線程的同步、鎖等概念。但,ThreadLocal雖出現(xiàn)在多線程的環(huán)境中,對于它的使用,并不涉及到鎖和同步的概念。它生于多線程,伴隨著多線程的熱點,而并不沾染多線程的常見問題,是不是莫名的小清新呢?如果你對它有所了解,聽說過內(nèi)存泄露,如何才能更好的駕馭它呢?帶著好奇和疑惑,一起深入ThreadLocal吧!
1.背景
隨便舉兩個具體的例子:
在一個web項目中,從請求一進(jìn)來就為之生成一個
uuid,無論系統(tǒng)是否報異常,返回給客戶的必須是同一uuid,可能首先會想到當(dāng)作方法的參數(shù)來傳遞,這樣任何地方可以成功的獲取到這個uuid,但這個uuid會在系統(tǒng)中幾乎各個方法參數(shù)中都會出現(xiàn),但uuid又非主要業(yè)務(wù)參數(shù),這樣勢必會與業(yè)務(wù)耦合性太強。-
對于很多非線程安全的類而言,如工具類:SimpleDateFormat和JDBC的Connection,它們經(jīng)常出現(xiàn)在并發(fā)環(huán)境中,例如Connection,大家剛接觸JDBC的時候,都是在方法中完成Connection的
init/commit/close,如多個線程都想連接數(shù)據(jù)庫執(zhí)行sql,方法有如下:- 對Connection進(jìn)行同步加鎖,協(xié)調(diào)各個線程操作DB的順序,沒錯但很低效。
- 每個線程自己創(chuàng)建Connection,會造成頻繁的創(chuàng)建和釋放連接,線程結(jié)束,Connection也就結(jié)束。
2. 與并發(fā)/同步的區(qū)別
什么,你突然想到了并發(fā)中的同步?先立一個flag,其實他們有本質(zhì)的區(qū)別,同步是協(xié)調(diào)多個線程對同一個變量的修改,而ThreadLocal則是將這個變量的副本據(jù)為線程己有,各個線程操作的是各自的threadlocal變量,各個線程互不影響,自然不會涉及到同步。
3. 易混名詞釋疑
大家容易搞混Thread/ThreadLocal/ThreadLocalMap三者的關(guān)系,其實很簡單,如圖:

為了便于大家記住依賴關(guān)系,煞費苦心的我編了個故事:從前,有一個雷劈出來一個線程小天(Thread),有一天,遇到了小樂(threadLocal),線程小天說:“如果想發(fā)揮價值,你必須初始化一個ThreadLocalMap放我這托管!” 小樂調(diào)用initialValue()不一會兒就初始化了ThreaLocalMap——小明(ThreadLocal的靜態(tài)內(nèi)部類),然后樂轉(zhuǎn)身給天說,這是小明,小天看著歡喜地說:“明明,我給你一個小名threadLocals吧(將其賦值給當(dāng)前線程的threadLocals變量),以后呢,你就跟我小天混,只要我還在,你就會有肉吃。你的工作內(nèi)容也很簡單,如果以后小樂調(diào)用get()方法獲取值的時候,你就將他的 threadLocalHashCode 作為key,在你的Map中找到對應(yīng)的value。當(dāng)然 set() 方法也差不多,頂多處理一下hash沖突的問題,不過這是你的內(nèi)務(wù),我就不干預(yù)了?!? 當(dāng)然,小天后面也有遇到其他的ThreadLocal,不過它已經(jīng)有Map小明了,直接讓小明干活就可以了。
4. 源碼時間
作為專業(yè)的看官,等的就是代碼,靜下心來,15min后,讓你感受到咸魚翻身,雖然還是咸魚,哈哈
來看ThreadLocal這個類,其中包括 get/set/remove 等方法,為了避免碼字嫌疑,只貼關(guān)鍵代碼(其中加入了筆者Norman的中文注釋,幫助理解),下面逐個介紹:
4.1 set()
代碼包含set()方法,同時包括方法體內(nèi)所調(diào)用的其他方法(后同)
// 代碼段1
public class ThreadLocal<T> {
//...
public void set(T value) {
// 獲取當(dāng)前線程t
Thread t = Thread.currentThread();
// 獲取當(dāng)前線程對應(yīng)的 ThreadLocalMap,它是ThreadLocal的一個內(nèi)部類
ThreadLocal.ThreadLocalMap map = getMap(t);
// 如果map之前被創(chuàng)建,則直接進(jìn)map中取值
if (map != null)
map.set(this, value);
// 創(chuàng)建ThreadLocalMap
else
createMap(t, value);
}
// 獲取線程t的ThreadLocalMap,無則return null
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 創(chuàng)建線程t的ThreadLocalMap,并設(shè)定初始值
void createMap(Thread t, T firstValue) {
// 創(chuàng)建新的ThreadLocalMap,并將其與當(dāng)前線程進(jìn)行關(guān)聯(lián),構(gòu)造方法往下翻
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
//...
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when wse have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table,INITIAL_CAPACITY為16
table = new Entry[INITIAL_CAPACITY];
// 將threadLocal的threadLocalHashCode除以16取模(下面這種騷操作是因為除數(shù)是2^n),得到桶的下標(biāo)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 將生成的Entry 放置于table對應(yīng)的桶中
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 設(shè)置擴(kuò)容閾值為table length的 2/3(負(fù)載因子2/3)
setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
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) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//...
}
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
* 這個數(shù)字就不得不提了,以他為步長生成的2^n個數(shù)字序列,
* 除以2^n取模后,得到的模居然可以逐個均勻的落在2^n個桶中,
* 與傳統(tǒng)步長(+1)的不同在于,逐個均勻分布(而非連續(xù)分布)可以減小碰撞的幾率,
* (可以拿著這個數(shù)應(yīng)用在其他類似場景中)
*/
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
// 從0開始遞增
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//...
}
// 代碼段2
public class Thread implements Runnable {
//...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
//...
}
整體流程可以看出,當(dāng)調(diào)用set(T value)方法時,會先取出本線程的ThreadLocalMap,對于Map:
- 如果不為空,則以ThreadLocal實例為key, 將value存儲在此Map中
- 如果為空,就創(chuàng)建一個Map,并將其賦值給此線程的成員變量threadLocals
對于ThreadLocalMap是由誰來維護(hù),其定義的代碼如下:
// 代碼段3
public class ThreadLocal<T> {
//...
static class ThreadLocalMap {
/**
* 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;
}
}
//...
}
//...
}
結(jié)合代碼段2,可以看出,ThreadLocalMap其實是定義在ThreadLocal中的靜態(tài)內(nèi)部類,然后由Thread類來維護(hù),依附于Thread的生命周期。讀過HashMap源碼的童鞋知道Entry是什么東東,這個Entry 繼承了 WeakReference類,其實就Entry的key繼承了它,從構(gòu)造函數(shù)就可以看出,順帶簡單回顧下java 的引用:
- 強引用:不受GC影響,即時OOM也不回收;eg. Person p = new Person("Norman")
- 軟引用:只會在內(nèi)存不足時,由GC回收;
- 弱引用:不論內(nèi)存是否夠用,一旦GC,則回收,不過GC的線程優(yōu)先級,不一定很快的發(fā)現(xiàn);
- 虛引用:形同虛設(shè),與前三不同,它必須配合WeakReferenceQueue,跟蹤對象被垃圾回收的活動
那么,為什么要用到弱引用呢?官方文檔如是說:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.
Norman譯:為了應(yīng)對非常大的和長壽命的對象使用,哈希表
Entry使用WeakReferences作為鍵。但是,由于沒有使用引用隊列(Reference類中的隊列), 因此只有當(dāng)表快耗盡空間時, 才保證將陳舊Entry刪除。
如下場景很好的解釋了這樣設(shè)計的好處(感謝xiaohansong):
強引用: 當(dāng)對象A中引用ThreadLocal的對象B,A被回收,則B變?yōu)槔?,但線程對Map是強引用,Map對B是 強 引用,只要線程存活,則B始終不會被回收。
弱引用: 當(dāng)對象A中引用ThreadLocal的對象B,A被回收,則B變?yōu)槔?,由于線程對Map是強引用,Map對B是 弱 引用,即使沒有手動刪除,在下一個GC周期,B也會被回收掉。而Map中的value會在調(diào)用set/get/remove方法后斷掉強引用,等待GC后續(xù)回收(見 4.4 內(nèi)存泄露)。
4.2 get()
public class ThreadLocal<T> {
//...
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
// 1.獲取當(dāng)前線程t
Thread t = Thread.currentThread();
// 2.獲取當(dāng)前線程對應(yīng)的 ThreadLocalMap, 它是ThreadLocal的一個內(nèi)部類
ThreadLocal.ThreadLocalMap map = getMap(t);
// 3.如果map之前被創(chuàng)建,則直接進(jìn)map中取值
if (map != null) {
// 3.1 以當(dāng)前ThreadLocal實例作為key,在map中獲取對應(yīng)的Entry
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 4.如果map之前未被創(chuàng)建,或創(chuàng)建后未得到該ThreadLocal實例的Entry
// 則調(diào)用此方法進(jìn)行初始化,而后返回結(jié)果
return setInitialValue();
}
static class ThreadLocalMap {
//...
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
// 如果不在,則按規(guī)則尋找
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;
}
//...
}
如果,當(dāng)前線程沒有threadLocal值,則默認(rèn)調(diào)用initialValue()方法,其中的取值可以看到,ThreadLocalMap中處理Hash沖突的方法是線性探測法,順帶回顧下數(shù)據(jù)結(jié)構(gòu)中,Hash沖突的解決辦法:
- 開放地址法
- 線性探測 (ThreadLocalMap)
- 二次探測
- 再哈希
- 再哈希法
- 鏈地址法 (HashMap)
- 建立一個公共溢出區(qū)
4.3 remove()
public class ThreadLocal<T> {
// ...
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 調(diào)用ThreadLocalMap的remove方法
m.remove(this);
}
// ...
static class ThreadLocalMap {
// ...
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 按hashcode計算出應(yīng)該在的位置
int i = key.threadLocalHashCode & (len - 1);
// 考慮到hash沖突,按線性探測查找應(yīng)該在的位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 判斷是否是目標(biāo)Entry
if (e.get() == key) {
// 調(diào)用Reference中的clear()方法,Clears this reference object.
e.clear();
// 將該位置的Entry清除掉后,對table重新整理
expungeStaleEntry(i);
return;
}
}
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 先將value引用置空
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
// 因為后續(xù)連續(xù)的元素可能是之前hash沖突引起的,
// 所以,對table后續(xù)連續(xù)的元素,進(jìn)行重新hash
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果key為空,順便清除它
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
// 如果一個元素按照hashcode運算后,
// 實際位置不在應(yīng)該在的位置,則對其重新hash
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;
}
}
}
4.4 內(nèi)存泄露
從源碼中看,無論是get(),set()還是remove()操作,都會包含對ThreadLocalMap 中key為null的Entry清除,那么泄露會出現(xiàn)在什么地方呢?仔細(xì)來看各部分依賴圖:

ThreadLocal可手動置為null,也可以由GC置null(因為弱引用),但這只是針對key,對于value,當(dāng)前Entry的value被Entry引用,而Entry被當(dāng)前Map引用,而Map則被當(dāng)前線程實例Thread引用,如果當(dāng)前線程不退出,則value是不會被GC,造成內(nèi)存泄露。
更加準(zhǔn)確的說,是發(fā)生在 :當(dāng)Map中的key(ThreadLocal)為null后到線程結(jié)束 這期間。當(dāng)遇到線程池,線程會被重復(fù)利用,如果使用 set 后不再使用 get/set/remove,這個強應(yīng)用會一直存在,造成內(nèi)存泄露。(PS:當(dāng)value是大對象時尤為嚴(yán)重)
那補救措施有哪些呢?
- 首先,jdk本身get/set/remove操作會清除key為null的Entry,但屬于被動清除,不調(diào)用此方法,依然會內(nèi)存泄露
- 其次,當(dāng)用完threadLocal后,應(yīng)該主動調(diào)用remove方法,主動斷掉value到thread的引用鏈
5. 總結(jié)
使用ThreadLocal有一些建議:
- 使用static修飾,使之屬于類而不是實例,因為它持有的對象,生效范圍一般在用戶會話/web請求周期期間。
One web request => one Persistence session.
Not one web request => one persistence session per object. - 如上文提到,使用結(jié)束后調(diào)用remove()方法進(jìn)行清除,避免造成內(nèi)存泄露。