ThreadLocal
ThreadLocal意為線程本地變量,用于解決多線程并發(fā)時訪問共享變量的問題。
所謂的共享變量指的是在堆中的實例、靜態(tài)屬性和數(shù)組;對于共享數(shù)據(jù)的訪問受Java的內(nèi)存模型(JMM)的控制,其模型如下:

每個線程都會有屬于自己的本地內(nèi)存,在堆(也就是上圖的主內(nèi)存)中的變量在被線程使用的時候會被復(fù)制一個副本線程的本地內(nèi)存中,當(dāng)線程修改了共享變量之后就會通過JMM管理控制寫會到主內(nèi)存中。
很明顯,在多線程的場景下,當(dāng)有多個線程對共享變量進(jìn)行修改的時候,就會出現(xiàn)線程安全問題,即數(shù)據(jù)不一致問題。常用的解決方法是對訪問共享變量的代碼加鎖(synchronized或者Lock)。但是這種方式對性能的耗費比較大。在JDK1.2中引入了ThreadLocal類,來修飾共享變量,使每個線程都單獨擁有一份共享變量,這樣就可以做到線程之間對于共享變量的隔離問題。
當(dāng)然鎖和ThreadLocal使用場景還是有區(qū)別的,具體區(qū)別如下:

一、ThreadLocal的使用及原理
1.1 使用
一般都會將ThreadLocal聲明成一個靜態(tài)字段,同時初始化如下:
static ThreadLocalthreadLocal =new ThreadLocal<>();
其中Object就是原本堆中共享變量的數(shù)據(jù)。
例如,有個User對象需要在不同線程之間進(jìn)行隔離訪問,可以定義ThreadLocal如下:
public class Test {
? ? static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}
常用的方法
set(T value):設(shè)置線程本地變量的內(nèi)容。
get():獲取線程本地變量的內(nèi)容。
remove():移除線程本地變量。注意在線程池的線程復(fù)用場景中在線程執(zhí)行完畢時一定要調(diào)用remove,避免在線程被重新放入線程池中時被本地變量的舊狀態(tài)仍然被保存。
public class Test {
? ? static ThreadLocal<User> threadLocal = new ThreadLocal<>();
? ? public void m1(User user) {
? ? ? ? threadLocal.set(user);
? ? }
? ? public void m2() {
? ? ? ? User user = threadLocal.get();
? ? ? ? // 使用
? ? ? ? // 使用完清除
? ? ? ? threadLocal.remove();
? ? }
}
1.2 原理
那么如何究竟是如何實現(xiàn)在每個線程里面保存一份單獨的本地變量呢?首先,在Java中的線程是什么呢?是的,就是一個Thread類的實例對象!而一個實例對象中實例成員字段的內(nèi)容肯定是這個對象獨有的,所以我們也可以將保存ThreadLocal線程本地變量作為一個Thread類的成員字段,這個成員字段就是:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
是一個在ThreadLocal中定義的Map對象,保存了該線程中的所有本地變量。ThreadLocalMap中的Entry的定義如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
? ? /** The value associated with this ThreadLocal. */
? ? Object value;
? ? // key為一個ThreadLocal對象,v就是我們要在線程之間隔離的對象
? ? Entry(ThreadLocal<?> k, Object v) {
? ? ? ? super(k);
? ? ? ? value = v;
? ? }
}
ThreadLocalMap和Entry都在ThreadLocal中定義。
ThreadLocal::set方法的原理
set方法的源碼如下:
public void set(T value) {
? ? // 獲取當(dāng)前線程
? ? Thread t = Thread.currentThread();
? ? // 獲取當(dāng)前線程的threadLocals字段
? ? ThreadLocalMap map = getMap(t);
? ? // 判斷線程的threadLocals是否初始化了
? ? if (map != null) {
? ? ? ? map.set(this, value);
? ? } else {
? ? ? ? // 沒有則創(chuàng)建一個ThreadLocalMap對象進(jìn)行初始化
? ? ? ? createMap(t, value);
? ? }
}
createMap方法的源碼如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
map.set方法的源碼如下:
/**
* 往map中設(shè)置ThreadLocal的關(guān)聯(lián)關(guān)系
* set中沒有使用像get方法中的快速選擇的方法,因為在set中創(chuàng)建新條目和替換舊條目的內(nèi)容一樣常見,
* 在替換的情況下快速路徑通常會失敗(對官方注釋的翻譯)
*/
private void set(ThreadLocal<?> key, Object value) {
? ? // map中就是使用Entry[]數(shù)據(jù)保留所有的entry實例
? ? Entry[] tab = table;
? ? int len = tab.length;
? ? // 返回下一個哈希碼,哈希碼的產(chǎn)生過程與神奇的0x61c88647的數(shù)字有關(guān)
? ? 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) {
? ? ? ? ? ? // 已經(jīng)存在則替換舊值
? ? ? ? ? ? e.value = value;
? ? ? ? ? ? return;
? ? ? ? }
? ? ? ? if (k == null) {
? ? ? ? ? ? // 在設(shè)置期間清理哈希表為空的內(nèi)容,保持哈希表的性質(zhì)
? ? ? ? ? ? replaceStaleEntry(key, value, i);
? ? ? ? ? ? return;
? ? ? ? }
? ? }
? ? tab[i] = new Entry(key, value);
? ? int sz = ++size;
? ? // 擴(kuò)容邏輯
? ? if (!cleanSomeSlots(i, sz) && sz >= threshold)
? ? ? ? rehash();
}
Thread::get方法的原理
public T get() {
? ? Thread t = Thread.currentThread();
? ? ThreadLocalMap map = getMap(t);
? ? if (map != null) {
? ? ? ? // 獲取ThreadLocal對應(yīng)保留在Map中的Entry對象
? ? ? ? ThreadLocalMap.Entry e = map.getEntry(this);
? ? ? ? if (e != null) {
? ? ? ? ? ? @SuppressWarnings("unchecked")
? ? ? ? ? ? // 獲取ThreadLocal對象對應(yīng)的值
? ? ? ? ? ? T result = (T)e.value;
? ? ? ? ? ? return result;
? ? ? ? }
? ? }
? ? // map還沒有初始化時創(chuàng)建map對象,并設(shè)置null,同時返回null
? ? return setInitialValue();
}
ThreadLocal::remove()方法原理
public void remove() {
? ? ThreadLocalMap m = getMap(Thread.currentThread());
? ? // 鍵在直接移除
? ? if (m != null) {
? ? ? ? m.remove(this);
? ? }
}
ThreadLocalMap的類結(jié)構(gòu)體系如下:

1.3 ThreadLocal設(shè)計
在JDK早期的設(shè)計中,每個ThreadLocal都有一個map對象,將線程作為map對象的key,要存儲的變量作為map的value,但是現(xiàn)在已經(jīng)不是這樣了。
JDK8之后,每個Thread維護(hù)一個ThreadLocalMap對象,這個Map的key是ThreadLocal實例本身,value是存儲的值要隔離的變量,是泛型,其具體過程如下:
每個Thread線程內(nèi)部都有一個Map(ThreadLocalMap::threadlocals);
Map里面存儲ThreadLocal對象(key)和線程的變量副本(value);
Thread內(nèi)部的Map由ThreadLocal維護(hù),由ThreadLocal負(fù)責(zé)向map獲取和設(shè)置變量值;
對于不同的線程,每次獲取副本值時,別的線程不能獲取當(dāng)前線程的副本值,就形成了數(shù)據(jù)之間的隔離。
JDK8之后設(shè)計的好處在于:
每個Map存儲的Entry的數(shù)量變少,在實際開發(fā)過程中,ThreadLocal的數(shù)量往往要少于Thread的數(shù)量,Entry的數(shù)量減少就可以減少哈希沖突。
當(dāng)Thread銷毀的時候,ThreadLocalMap也會隨之銷毀,減少內(nèi)存使用,早期的ThreadLocal并不會自動銷毀。
使用ThreadLocal的好處
保存每個線程綁定的數(shù)據(jù),在需要的地方可以直接獲取,避免直接傳遞參數(shù)帶來的代碼耦合問題;
各個線程之間的數(shù)據(jù)相互隔離卻又具備并發(fā)性,避免同步方式帶來的性能損失。
二、ThreadLocal內(nèi)存泄露問題
內(nèi)存泄露問題:指程序中動態(tài)分配的堆內(nèi)存由于某種原因沒有被釋放或者無法釋放,造成系統(tǒng)內(nèi)存的浪費,導(dǎo)致程序運行速度減慢或者系統(tǒng)奔潰等嚴(yán)重后果。內(nèi)存泄露堆積將會導(dǎo)致內(nèi)存溢出。
ThreadLocal的內(nèi)存泄露問題一般考慮和Entry對象有關(guān),在上面的Entry定義可以看出ThreadLocal::Entry被弱引用所修飾。**JVM會將弱引用修飾的對象在下次垃圾回收中清除掉。**這樣就可以實現(xiàn)ThreadLocal的生命周期和線程的生命周期解綁。但實際上并不是使用了弱引用就A會發(fā)生內(nèi)存泄露問題,考慮下面幾個過程:
使用強(qiáng)引用

當(dāng)ThreadLocal Ref被回收了,由于在Entry使用的是強(qiáng)引用,在Current Thread還存在的情況下就存在著到達(dá)Entry的引用鏈,無法清除掉ThreadLocal的內(nèi)容,同時Entry的value也同樣會被保留;也就是說就算使用了強(qiáng)引用仍然會出現(xiàn)內(nèi)存泄露問題。
使用弱引用

當(dāng)ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的時候就會將ThreadLocal對象清除,這個時候Entry中的KEY=null。但是由于ThreadLocalMap中任然存在Current Thread Ref這個強(qiáng)引用,因此Entry中value的值任然無法清除。還是存在內(nèi)存泄露的問題。
由此可以發(fā)現(xiàn),使用ThreadLocal造成內(nèi)存泄露的問題是因為:ThreadLocalMap的生命周期與Thread一致,如果不手動清除掉Entry對象的話就可能會造成內(nèi)存泄露問題。因此,需要我們在每次在使用完之后需要手動的remove掉Entry對象。
那么為什么使用弱引用?
避免內(nèi)存泄露的兩種方式:使用完ThreadLocal,調(diào)用其remove方法刪除對應(yīng)的Entry或者使用完ThreadLocal,當(dāng)前Thread也隨之運行結(jié)束。第二種方法在使用線程池技術(shù)時是不可以實現(xiàn)的。
所以一般都是自己手動調(diào)用remove方法,調(diào)用remove方法弱引用和強(qiáng)引用都不會產(chǎn)生內(nèi)存泄露問題,使用弱引用的原因如下:
在ThreadLocalMap的set/getEntry中,會對key進(jìn)行判斷,如果key為null,那么value也會被設(shè)置為null,這樣即使在忘記調(diào)用了remove方法,當(dāng)ThreadLocal被銷毀時,對應(yīng)value的內(nèi)容也會被清空。多一層保障!
總結(jié):存在內(nèi)存泄露的有兩個地方:ThreadLocal和Entry中Value;最保險還是要注意要自己及時調(diào)用remove方法?。?!
三、ThreadLocal的應(yīng)用場景
場景一:在重入方法中替代參數(shù)的顯式傳遞
假如在我們的業(yè)務(wù)方法中需要調(diào)用其他方法,同時其他方法都需要用到同一個對象時,可以使用ThreadLocal替代參數(shù)的傳遞或者static靜態(tài)全局變量。這是因為使用參數(shù)傳遞造成代碼的耦合度高,使用靜態(tài)全局變量在多線程環(huán)境下不安全。當(dāng)該對象用ThreadLocal包裝過后,就可以保證在該線程中獨此一份,同時和其他線程隔離。
例如在Spring的@Transaction事務(wù)聲明的注解中就使用ThreadLocal保存了當(dāng)前的Connection對象,避免在本次調(diào)用的不同方法中使用不同的Connection對象。
場景二:全局存儲用戶信息
可以嘗試使用ThreadLocal替代Session的使用,當(dāng)用戶要訪問需要授權(quán)的接口的時候,可以現(xiàn)在攔截器中將用戶的Token存入ThreadLocal中;之后在本次訪問中任何需要用戶用戶信息的都可以直接沖ThreadLocal中拿取數(shù)據(jù)。例如自定義獲取用戶信息的類AuthHolder:
public class AuthNHolder {
? ? private static final ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>();
? ? public static void map(Map<String,String> map){
? ? ? ? threadLocal.set(map);
? ? }
? ? // 獲取用戶id
? ? public static String userId(){
? ? ? ? return get("userId");
? ? }
? ? // 根據(jù)鍵值獲取對應(yīng)的信息
? ? public static String get(String key){
? ? ? ? Map<String,String> map = getMap();
? ? ? ? return map.get(key);
? ? }
? ? // 用完清空ThreadLocal
? ? public static void clear(){
? ? ? ? threadLocal.remove();
? ? }
}
備注:參考博文https://cloud.tencent.com/developer/article/1636025。ThreadLocal里面封裝的value只是一個例子,根據(jù)具體業(yè)務(wù)需求改就行了。
場景三:解決線程安全問題
依賴于ThreadLocal本身的特性,對于需要進(jìn)行線程隔離的變量可以使用ThreadLocal進(jìn)行封裝。
四、總結(jié)
ThreadLocal更像是對其他類型變量的一層包裝,通過ThreadLocal的包裝使得該變量可以在線程之間隔離和當(dāng)前線程全局共享。
線程的隔離性和變量的線程全局共享性得益于在每個Thread類中的threadlocals字段。(從類實例對象的角度抽象的去看Java中的線程?。。。?/p>
ThreadLocalMap中Entry的Key不管是否使用弱引用都有內(nèi)存泄露的可能。引起內(nèi)存泄露主要在于ThreadLocal對象和Entry中的Value對象,因此要確保每次使用完之后都remove掉Entry!