什麼是ThreadLocal?
通常情況下,我們建立的變數是可以被任何一個執行緒訪問並修改的。如果想實現每一個執行緒都有自己的專屬本地變數該如何解決呢?
JDK 中自帶的ThreadLocal
類正是為了解決這樣的問題。 ThreadLocal
類主要解決的就是讓每個執行緒繫結自己的值,可以將ThreadLocal
類形象的比喻成存放資料的盒子,盒子中可以儲存每個執行緒的私有資料。
如果你建立了一個ThreadLocal
變數,那麼訪問這個變數的每個執行緒都會有這個變數的本地副本,這也是ThreadLocal
變數名的由來。他們可以使用 get()
和 set()
方法來獲取預設值或將其值更改為當前執行緒所存的副本的值,從而避免了執行緒安全問題
ThreadLocal 原理了解嗎?
最終的變數是放在了當前執行緒的 ThreadLocalMap
中,並不是存在 ThreadLocal
上,ThreadLocal
可以理解為只是ThreadLocalMap
的封裝,傳遞了變數值。 ThreadLocal
類中可以透過Thread.currentThread()
獲取到當前執行緒物件後,直接透過getMap(Thread t)
可以訪問到該執行緒的ThreadLocalMap
物件。
每個Thread
中都具備一個ThreadLocalMap
,而ThreadLocalMap
可以儲存以ThreadLocal
為 key ,Object 物件為 value 的鍵值對。
在
ThreadLocal
類中有靜態內部類ThreadLocalMap
,在ThreadLocalMap
類中也有靜態內部類Entry
,而這個Entry
類繼承自WeakReference
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//.....
}
比如我們在同一個執行緒中宣告瞭兩個 ThreadLocal
物件的話, Thread
內部都是使用僅有的那個ThreadLocalMap
存放資料的,ThreadLocalMap
的 key 就是 ThreadLocal
物件,value 就是 ThreadLocal
物件呼叫set
方法設定的值。
ThreadLocal
資料結構如下圖所示:
在每條執行緒Thread內部有一個ThreadLocal.ThreadLocalMap型別的成員變數threadLocals,這個threadLocals就是每條執行緒用來儲存變數副本的,key值為當前ThreadLocal物件,value為變數副本(即T型別的變數)。每個Thread執行緒物件最開始的threadLocals都為空,當執行緒呼叫ThreadLocal.set()或ThreadLocal.get()方法時(get方法待會而會分析到),都會呼叫createMap()方法對threadLocals進行初始化。然後在當前執行緒裡面,如果要使用副本變數,就可以透過get方法在threadLocals裡面查詢。
ThreadLocalMap
ThreadLocal
的原理是涉及三個核心類:ThreadLocal
、Thread
以及ThreadLocalMap
類。在Thread
類中存在兩個成員變數:threadLocals
與inheritableThreadLocals
,這兩個成員變數的型別都為ThreadLocalMap
,經過一系列分析後我們可以得知,這兩個成員變數是儲存執行緒變數副本的最終容器,而前面也曾提到過:ThreadLocalMap
是ThreadLocal
中定製版的HashMap
,但是它並沒有實現Map
介面,而是自己內部透過陣列型別儲存Entry
實現。而Entry
只是簡單的繼承了WeakReference
弱引用,並沒有沒有實現類似HashMap
中Node.next
的後繼節點指向,所以ThreadLocalMap
並不是連結串列形式的實現。哪沒有了連結串列結構之後,ThreadLocalMap
是如何解決雜湊衝突的呢?
ThreadLocalMap
是如何解決雜湊衝突的呢? ---開放定址法
在呼叫createMap()
方法建立ThreadLocalMap
示例時,在ThreadLocalMap
的構造方法中,會為成員變數table
初始化一個長度為16的Entry
陣列,透過hashCode
與length
位運算確定出一個下標索引值i
,這個i
就是被儲存在table
陣列中的下標位置。
每條執行緒的threadlocals
都會在內部維護獨立table
陣列,而每個ThreadLocal
物件在不同的執行緒table
中位置都是相同的。對於同一條執行緒而言,不同的ThreadLocal
變數副本都會被封裝成一個個的Entry
物件儲存在自己內部的table
中。
ok~,接著往下說,經過int i = key.threadLocalHashCode & (len-1);
計算出索引下標值之後,會開始遍歷table
,然後會開始判斷,如果table[i]
位置不為空,但是原本的key值和現在新的key值是相同的情況下,則使用現在的新值替換掉之前的老值,重新整理value值並返回;如果table[i]
位置為空,則建立一個的Entry
物件封裝K-V
值並將該物件放在table[i]
位置;如果table[i]
位置不為空並且Key
不相同時,哪就呼叫nextIndex(i,len)
獲取下一個位置資訊並判斷下一個位置是否為空,直到找到為空的位置為止;在table[i]
位置不為空並且Key
不相同的情況下,如果遍歷完整個table
陣列也沒有找到為空的下標位置時,代表陣列已經存滿了需要擴容,則呼叫rehash()
對陣列擴容兩倍
整個ThreadLocalMap儲存過程結束,如下:
在get時,也會根據ThreadLocal
物件的雜湊值跟table
陣列長度進行計算獲取下標索引值i
,然後判斷該位置Entry
物件的key值與get(key)
的key是否相同,如果相同則直接獲取該位置的值並返回。如果不相同則遍歷整個陣列中table[i]
之後的所有元素,迴圈判斷下一個位置的key是否與傳入進來的key一致,如果一致則獲取返回
ThreadLocal 記憶體洩露問題是怎麼導致的?
ThreadLocalMap
中使用的 key 為 ThreadLocal
的弱引用,而 value 是強引用。所以,如果 ThreadLocal
沒有被外部強引用的情況下,在垃圾回收的時候,key 會被清理掉,而 value 不會被清理掉。
這樣一來,ThreadLocalMap
中就會出現 key 為 null 的 Entry。假如我們不做任何措施的話,value 永遠無法被 GC 回收,這個時候就可能會產生記憶體洩露。ThreadLocalMap
實現中已經考慮了這種情況,在呼叫 set()
、get()
、remove()
方法的時候,會清理掉 key 為 null 的記錄。使用完 ThreadLocal
方法後最好手動呼叫remove()
方法
可不可以把value也變成弱引用?
不可以。因為存進ThreadLocal中正在使用的物件,線上程的棧中也有引用的,這是一根強引用指標,所以只要執行緒還在使用,就算記憶體不足,對應的Key也不會被回收;反之,如果key和value的關係都設計成弱引用,這時假設記憶體不足,觸發GC就會導致value被回收,因為執行緒本身不直接持有value,而是透過key來間接性的訪問value,如果value也是弱引用,就會出現“key還在,value因為記憶體不足,導致被GC回收”的問題
可不可以把key變成強引用?
不可以。既然key被設計成了弱引用,所以才會導致key=null的情況出現,那假設把key設計成強引用,是不是就解決了這個問題呢?先看個例子:
public static void main(String[] args) {
ThreadLocal TL = new ThreadLocal();
TL.set(new Object());
TL = null;
}
這裡建立了一個ThreadLocal物件TL,並設定一個Object物件,然後將其置空。如果Key是強引用的話,TL無法被回收,也無法被訪問,Object無法被回收,也無法被訪問,Key和Value同時出現了記憶體洩漏。
為啥K-V都記憶體洩漏了呢?因為最後一行置空程式碼,只能將main執行緒棧中的引用置空,而Thread物件內部有一個threadLocals成員,依舊會保持與ThreadLocalMap的引用,而Map的Key又強引用自ThreadLocal,這時main執行緒的棧,雖然沒有引用這個TL,但Map卻在引用著它,最終就導致了K-V都記憶體洩漏。
上面也是ThreadLocalMap中,為什麼Key被設計成弱引用的原因,而且ThreadLocal也在儘可能的避免記憶體洩漏,當你呼叫set/get/remove()方法時,都會清理過期的Key(呼叫remove方法是最有效的)
綜上所述:key設計成弱引用反而是最好的選擇
ThreadLocalMap擴容機制瞭解嗎?
- 觸發擴容的條件
ThreadLocalMap
的初始容量是 16,它在儲存元素時,當元素個數達到閾值(threshold
)就會觸發擴容。閾值的計算方式是陣列容量(table.length
)的三分之二。- 例如,初始容量為 16 時,當儲存的元素個數達到
16 * 2/3 = 10
(向下取整)個元素時,就會觸發擴容。
- 擴容過程
- 擴容是建立一個新的
Entry
陣列,新陣列的大小是原來的兩倍。 - 然後遍歷舊陣列中的所有
Entry
,將其重新雜湊(rehash)到新陣列中。在重新雜湊的過程中,會處理可能出現的雜湊衝突。 - 對於雜湊衝突,
ThreadLocalMap
採用線性探測法來解決。即當發生衝突時,會順序查詢下一個可用的位置來儲存元素。在擴容後的重新雜湊過程中,這個線性探測的邏輯也會起作用。 - 假設舊陣列中有一個
Entry
在位置i
,重新雜湊時,它會先計算新的索引位置i' = i & (newLength - 1)
(其中newLength
是新陣列的長度),如果這個位置沒有被佔用,就將Entry
放入該位置;如果被佔用了,就會線性探測下一個位置,直到找到一個空閒位置。
- 擴容是建立一個新的
ThreadLocal怎麼實現執行緒隔離的?
由於每個執行緒都有自己獨立的ThreadLocalMap
,所以不同執行緒之間的ThreadLocal
變數是相互隔離的。即使多個執行緒使用了相同的ThreadLocal
物件,它們所操作的也是各自執行緒中的變數副本,不會相互影響。
要說是怎麼實現執行緒隔離的,其實就是在set()、get()方法的具體實現,我們set的值,為什麼不會被其他的執行緒所讀取。
Set()方法:
public void set(T value) {
// 1、獲取當前執行緒
Thread t = Thread.currentThread();
// 2、獲取當前執行緒的threadlocals成員變數
ThreadLocalMap map = getMap(t);
// 3、判斷map是否為null
if (map != null)
// 如果不為null,就直接將value放進map中
// key是當前的threadLocal,value就是傳進來的值
map.set(this, value);
else
// 如果為 null,初始化一個map,再將value 放進map中
// key是當前的threadLocal,value就是傳進來的值
createMap(t, value);
}
Get()方法:
public T get() {
// 獲取到當前執行緒
Thread t = Thread.currentThread();
// 2、獲取當前執行緒的threadlocals成員變數
ThreadLocalMap map = getMap(t);
//3、判斷map是否為null
if (map != null)
//3.1、如果不為null,根據當前的ThreadLocal 從當前執行緒中的ThreadLocals中取出map儲存的變數副本
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果儲存的值不為null,就返回值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//
return setInitialValue();
}
- 當在一個執行緒中呼叫
ThreadLocal
的get()
方法獲取變數值時,它會首先獲取當前執行緒的ThreadLocalMap
,然後根據當前的ThreadLocal
物件作為鍵,從ThreadLocalMap
中查詢對應的變數值並返回。如果找不到,則會返回null
或根據初始化方法返回預設值。
這就是ThreadLocal的原理~~~❤️❤️❤️