死磕Java源碼之ThreadLocal實現(xiàn)分析
通俗的講, ThreadLocal
是Java里一種特殊的變量蜘腌。每個線程都有一個ThreadLocalMap沫屡,用來存放ThreadLocal變量表,當(dāng)然這里不是直接通過Map的方式存儲撮珠,而是通過一個table和Entry結(jié)構(gòu)存儲
因為ThreadLocalMap
變量是跟線程綁定的沮脖,所以不存在多線程共享變量之間的并發(fā)問題,所以ThreadLocal
也就是線程安全的變量芯急。
具體的結(jié)構(gòu)倘潜,在源碼部分說明
ThreadLocal的使用
ThreadLocal主要有以下幾個方法:
public T get() { } // 用來獲取ThreadLocal在當(dāng)前線程中保存的變量副本
public void set(T value) { } //set()用來設(shè)置當(dāng)前線程中變量的副本
public void remove() { } //remove()用來移除當(dāng)前線程中變量的副本
protected T initialValue() { } //initialValue()是一個protected方法,一般是用來在使用時進行重寫的
寫一個demo志于,在main
線程和新建線程中涮因,對同一個ThreadLocal
變量進行修改,看下修改后的結(jié)果:
public class ThreadLocalDemo {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ThreadLocalDemo.threadLocal.set("hello world main");
System.out.println(ThreadLocalDemo.threadLocal.get());
try {
Thread thread = new Thread() {
public void run() {
ThreadLocalDemo.threadLocal.set("hello world thread");
System.out.println(ThreadLocalDemo.threadLocal.get());
};
};
thread.start();
thread.join();
} catch (Exception ex) {
System.out.println(ex);
}
System.out.println(ThreadLocalDemo.threadLocal.get());
}
}
執(zhí)行輸出:
hello world main
hello world thread
hello world main
不難看出伺绽,我們在new Thread()
中對ThreadLocal
的變量threadLocal
進行修改后养泡,在main線程中再次輸出嗜湃,其值并沒有收到影響,他們修改的分別是各自的副本澜掩,不會對其他副本有影響购披。
當(dāng)然這里完整的邏輯是應(yīng)該在使用完調(diào)用remove
方法刪除threadLocal
副本,以防內(nèi)存泄露肩榕。
具體原理見下文
ThreadLocal的內(nèi)存泄漏與源碼分析
ThreadLocal的結(jié)構(gòu)如圖:
ThreadLocal為什么會內(nèi)存泄漏
每個Thread
維護一個 ThreadLocalMap
映射表刚陡,這個映射表的 key
是 ThreadLocal
實例本身,value
是真正需要存儲的 Object
株汉。
也就是說 ThreadLocal
本身并不存儲值筐乳,它只是作為一個 key
來讓線程從 ThreadLocalMap
獲取 value
。值得注意的是圖中的虛線乔妈,表示 ThreadLocalMap
是使用 ThreadLocal
的弱引用作為 Key
的蝙云,弱引用的對象在 GC 時會被回收。
ThreadLocalMap
使用ThreadLocal
的弱引用作為key
路召,如果一個ThreadLocal
沒有外部強引用來引用它勃刨,那么系統(tǒng) GC 的時候,這個ThreadLocal
勢必會被回收股淡,這樣一來身隐,ThreadLocalMap
中就會出現(xiàn)key
為null
的Entry
,就沒有辦法訪問這些key
為null
的Entry
的value
唯灵,如果當(dāng)前線程再遲遲不結(jié)束的話贾铝,這些key
為null
的Entry
的value
就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成內(nèi)存泄漏早敬。
其實忌傻,ThreadLocalMap
的設(shè)計中已經(jīng)考慮到這種情況大脉,也加上了一些防護措施:在ThreadLocal
的get()
,set()
,remove()
的時候都會清除線程ThreadLocalMap
里所有key
為null
的value
搞监,當(dāng)然這只是一種防護措施,最好的使用方法是在用完了ThreadLocal變量時镰矿,調(diào)用remove()方法主動將ThreadLocal和value釋放琐驴。
關(guān)于為什么ThreadLocalMap
使用ThreadLocal
的弱引用,這就跟弱引用的機制有關(guān)秤标,若引用的對象在JVM執(zhí)行GC的時候就會被回收掉。通過gc前后查看table中對應(yīng)entry對象的referent即可查看是否被回收(示例的前提是ThreadLocal的強引用對象已經(jīng)釋放):
GC之前:
GC之后:
此時Entry的referent=null,當(dāng)再次通過調(diào)用get轨功、set曼月、remove方法是,ThreadLocal會有各自的機制衙猪,將Map中key(referent)為空的Entry移除馍乙,并釋放其中的value布近,一定成都避免了內(nèi)存泄漏,此機制源碼分析階段說明丝格。
ThreadLocal源碼分析
ThreadLocal中的關(guān)鍵屬性
//創(chuàng)建ThreadLocal時撑瞧,復(fù)制的HashCode
//HashCode是在全局靜態(tài)的nextHashCode基礎(chǔ)上增加一個HASH_INCREMENT而來
private final int threadLocalHashCode = nextHashCode();
下面分析幾個具體的函數(shù):
- get方法的實現(xiàn)
/**
* 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() {
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();
}
首先是當(dāng)前線程,然后通過getMap(t)
方法獲取到一個map显蝌,map的類型為ThreadLocalMap
预伺。然后接著下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進去的是 this曼尊,而不是當(dāng)前線程t酬诀。如果獲取成功,則返回value值涩禀。如果map為空料滥,則調(diào)用setInitialValue
方法返回value。
- setInitialValue方法的實現(xiàn)
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;
}
/**
* 構(gòu)造ThreadLocalMap
**/
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);
}
首先是通過調(diào)用initialValue
艾船,initialValue
是protected方法葵腹,初始化ThreadLocal
時可以重寫此函數(shù),相當(dāng)于延遲加載屿岂,然后通過getMap創(chuàng)建threadLocals
践宴,如果threadLocals
不存在時,會調(diào)用createMap創(chuàng)建一個初始大小為16的Entry數(shù)組table爷怀,并新建一個Entry存入table中阻肩。這個threadLocals就是用來存儲實際的變量副本的,鍵值為當(dāng)前ThreadLocal
變量运授,value為變量副本(即T類型的變量)
這里重點看下Entry類
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry
類是集成自WeakReference
烤惊,然后使用ThreadLocal作為了鍵,也就是說這里的ThreadLocal是一個弱引用在GC的時候會被回收吁朦。
接上文柒室,如果map存在,則會調(diào)用map的getEntry
方法逗宜,getEntry
方法實現(xiàn):
private Entry getEntry(ThreadLocal<?> key) {
//通過hash算出數(shù)組下標(biāo)
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
//如果取出Entry雄右,并且e.get也就是referent與threadLocal相同,則說明是需要的值纺讲,返回Entry對象e 擂仍,判斷e.get() = key 是解決hash碰撞的情況
return e;
else
//如果下標(biāo)i的Entry不存在或者 其threadLocal不相同,則執(zhí)行此
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
//說明有此entry熬甚,可能是hash碰撞的結(jié)果
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
//處理已無引用的ThreadLocal變量等逢渔,解決內(nèi)存泄漏的機制之一
expungeStaleEntry(i);
else
//下標(biāo)+1
i = nextIndex(i, len);
e = tab[i];
}
//如果getEntry中獲取的entry=null,則說明無此ThreadLocal變量乡括,返回null
return null;
}
expungeStaleEntry 方法
//刪除可以釋放的Entry
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) {
//如果發(fā)現(xiàn)ThreadLocal已經(jīng)被釋放掉肃廓,則通過這里來釋放value的引用冲簿,以及刪除數(shù)組table中的Entry
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
//重新設(shè)置Entry在table中的位置
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;
}
通過對get方法的大致分析,可以分為幾個階段:
1)判斷Map是否存在亿昏,如果不存在初始化Map以及table等
2)如果已存在峦剔,并且獲取到Entry,則返回
3)如果不存在角钩,則調(diào)用expungeStaleEntry
清除需要釋放的ThreadLocal
吝沫、釋放對value的一用,從table中刪除相應(yīng)下標(biāo)的Entry递礼,以及重新設(shè)置元素在table中的位置
- set方法的實現(xiàn)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set方法中的createMap與上文的createMap相同惨险,不在做說明,重點看下map.set(this, value); 這里直接在溫中鋒
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) {
//如果通過hash計算的下標(biāo)取出的entry的key與設(shè)置的相同脊髓,則更新value
e.value = value;
return;
}
if (k == null) {
//和HashMap不一樣辫愉,由于Entry key繼承了軟引用,會出現(xiàn)k是null的情況将硝!所以會接著在replaceStaleEntry重新循環(huán)尋找相同的key
replaceStaleEntry(key, value, i);
return;
}
}
//如果key!= null 并且 k != key 說明存在hash鵬錚
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//調(diào)用cleanSomeSlots()對table進行清理恭朗,如果沒有任何Entry被清理,并且表的size超過了閾值依疼,就會調(diào)用rehash()方
rehash();
}
hash散列的鍵值數(shù)據(jù)在存儲過程中可能會發(fā)生碰撞痰腮,大家知道HashMap存儲的是一個Entry鏈,當(dāng)hash發(fā)生沖突后律罢,將新的Entry存放在鏈表最前端膀值。但是ThreadLocalMap
不一樣,采用index+1作為重散列的hash值寫入误辑。另外有一點需要注意key出現(xiàn)null的原因是由于Entry的key是繼承了軟引用沧踏,在下一次GC時不管它有沒有被引用都會被回收掉而Value沒有被回收。當(dāng)出現(xiàn)null時巾钉,會調(diào)用replaceStaleEntry()
方法接著循環(huán)尋找相同的key翘狱,如果存在,直接替換舊值睛琳。如果不存在盒蟆,則在當(dāng)前位置上重新創(chuàng)建新的Entry.
remove方法的實現(xiàn)
-
//ThreadLocal public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } /** * ThreadeLocalMap * 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) { e.clear(); expungeStaleEntry(i); return; } } }
remove方法相對簡單踏烙,通過hashcode計算出下標(biāo)师骗,然后判斷key與要刪除的ThreadLocal是否一致,如果一致讨惩,釋放掉相應(yīng)的引用辟癌,并調(diào)用expungeStaleEntry方法清理其他的可以釋放的對象。
ThreadLocal的使用場景
- 每個線程自己獨享的數(shù)據(jù)荐捻,比如session數(shù)據(jù)
- 實例需要在多個方法中共享黍少,但不希望被多線程共享
比如在Dubbo中的RpcContext
實例寡夹,在RpcContext.java
文件中,通過靜態(tài)的ThreadLocal
變量厂置,為每個線程持有一個RpcContext對象菩掏,這個RpcContext
對象只有在此線程的不同方法中共享使用,在多線程中不會共享昵济,是一種典型的應(yīng)用智绸,包括重寫了initialValue
方法
rivate static final ThreadLocal<RpcContext> LOCAL = new ThreadLocal<RpcContext>() {
@Override
/**
* 重新initialValue方法,當(dāng)get時為null時访忿,通過回調(diào)此方法獲取RpcContext實例
**/
protected RpcContext initialValue() {
return new RpcContext();
}
};
/**
* get context.
*
* @return context
*/
public static RpcContext getContext() {
return LOCAL.get();
}
/**
* remove context.
*
* @see com.alibaba.dubbo.rpc.filter.ContextFilter
*/
public static void removeContext() {
LOCAL.remove();
}
總結(jié)
-
ThreadLocal
并不解決線程間共享數(shù)據(jù)的問題瞧栗,通過使用ThreadLocal是使數(shù)據(jù)在不同線程有不同的副本,不會有多線程共享數(shù)據(jù)也就不需要解決共享數(shù)據(jù)的問題 - 每個線程持有一個 Map 并維護了
ThreadLocal
對象與具體實例的映射海铆,該 Map 由于只被持有它的線程訪問迹恐,故不存在線程安全以及鎖的問題 -
ThreadLocalMap
的Entry
對ThreadLocal
的引用為弱引用,避免了因ThreadLocalMap強引用 ThreadLocal 對象在線程回收之前無法被回收的問題 -
ThreadLocalMap
的 set 方法通過調(diào)用 replaceStaleEntry 方法回收鍵為 null 的 Entry 對象的值(即為具體實例)以及 Entry 對象本身從而防止內(nèi)存泄漏 -
ThreadLocalMap
的 get 方法通過調(diào)用expungeStaleEntry
方法回收鍵為 null 的 Entry 對象的值(即為具體實例)以及 Entry 對象本身從而防止內(nèi)存泄漏 -
ThreadLocalMap
當(dāng)hash發(fā)生沖突后卧斟,并不是與HashMap一樣采用的Entry鏈表將新的Entry存放在鏈表最前端殴边。而是采用index+1作為重散列的hash值來重新存儲Entry值
文章部分自己理解,部分借鑒了大牛們的文章珍语,在此表示感謝找都!如有bug,勞煩指正廊酣!
參考:
https://blog.csdn.net/liulongling/article/details/50607802
http://www.importnew.com/21206.html
http://www.jasongj.com/java/threadlocal/