ThreadLocal原理及使用場景

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!


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容