多執行緒學習筆記九之ThreadLocal
多執行緒學習筆記九之ThreadLocal
簡介
ThreadLocal顧名思義理解為執行緒本地變數,這個變數只在這個執行緒內,對於其他的執行緒是隔離的,JDK中對ThreadLocal的介紹:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its{@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
大意是ThreadLocal提供了執行緒區域性變數,只能通過ThreadLocal的set方法和get方法來儲存和獲得變數。
類結構
ThreadLocal類結構如下:
可以看到ThreadLocal有內部類ThradLocalMap,ThreadLocal儲存執行緒區域性物件就是利用了ThreadLocalMap資料結構,在下面的原始碼分析也會先從這裡開始。
原始碼分析
ThreadLocalMap
ThreadLocalMap靜態內部類Entry是儲存鍵值對的基礎,Entry類繼承自WeakReference(為什麼用弱引用在後面解釋),通過Entry的構造方法表明鍵值對的鍵只能是ThreadLocal物件,值是Object型別,也就是我們儲存的執行緒區域性物件,通過super呼叫父類WeakReference建構函式將ThreadLocal<?>物件轉換成弱引用物件
ThreadMap儲存鍵值對的原理與HashMap是類似的,HashMap依靠的是陣列+紅黑樹資料結構和雜湊值對映,ThreadMap依靠Entry陣列+雜湊對映,ThreadLocalMap使用了Entry陣列來儲存鍵值對,Entry陣列的初始長度為16,鍵值對到Entry陣列的對映依靠的是 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
,通過ThreadLocal物件的threadLocalHashCode與(INITIAL_CAPACITY - 1)按位相與將鍵值對均勻雜湊到Entry陣列上。
static class ThreadLocalMap { // 鍵值對物件 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //初始Entry陣列大小 private static final int INITIAL_CAPACITY = 16; //Entry陣列 private Entry[] table; //ThreadLocalMap實際儲存鍵值對的個數 private int size = 0; //陣列擴容閾值 private int threshold; // Default to 0 //閾值為陣列長度的2/3 private void setThreshold(int len) { threshold = len * 2 / 3; } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } /** * Decrement i modulo len. */ private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } //構造一個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); } }
ThreadLocal作為做為鍵值對的鍵通過常量 threadLocalHashCode
對映到Entry陣列, threadLocalHashCode
初始化時會呼叫 nextHashCode()
方法,就是在 nextHashCode
的基礎上加上 0x61c88647
,實際上每個 ThreadLocal
物件的 threadLocalHashCode
值相差 0x61c88647
,這樣生成出來的Hash值可以較為均勻的雜湊到2的冪次方長度的陣列中,具體可見這篇文章 為什麼使用0x61c88647
由於採用的是雜湊演算法,就需要考慮Hash衝突的情況,HashMap解決Hash衝突的方法是連結串列+紅黑樹,ThreadLocalMap解決方法是linear-probe(線性探測),簡單來說如果雜湊對應的位置已經有鍵值對佔據了,就把雜湊位置加/減一找到符合條件的位置放置鍵值對。
// final常量,一旦確定不再改變 private final int threadLocalHashCode = nextHashCode(); /** * The next hash code to be given out. Updated atomically. Starts at * zero. */ 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. */ private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } //構造方法 public ThreadLocal() { }
set(T value)
簡要介紹完了內部類ThreadLocalMap後,set方法屬於ThreadLocal,首先獲得與執行緒Thread繫結的ThreadLocalMap物件,再將ThreadLocal和傳入的value封裝為Entry鍵值對存入ThreadLocalMap中。注意,ThreadLocalMap物件是線上程Thread中宣告的:
ThreadLocal.ThreadLocalMap threadLocals = null;
public void set(T value) { //獲得當前執行緒物件 Thread t = Thread.currentThread(); //獲得執行緒物件的ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果map存在,則將鍵值對存到map裡面去 if (map != null) map.set(this, value); //如果不存在,呼叫ThreadLocalMap構造方法儲存鍵值對 else createMap(t, value); } //返回執行緒t中宣告的Thread ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
-
set(ThreadLocal<?> key, Object value)
在ThreadLocalMap存在的情況下,呼叫ThreadLocal類的set方法儲存鍵值對,set方法需要考慮雜湊的位置已經有鍵值對:如果已經存在的鍵值對的鍵當存入的鍵,覆蓋鍵值對的值;如果鍵值對的鍵ThreadLocal物件已經被回收,呼叫replaceStaleEntry方法刪除table中所有陳舊的元素(即entry的引用為null)並插入新元素。
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; //利用ThreadLocal的threadLocalHahsCode值雜湊 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; } //鍵值對的鍵為空,說明鍵ThreadLocal物件被回收,用新的鍵值對代替過時的鍵值對 if (k == null) { replaceStaleEntry(key, value, i); return; } } //雜湊位置為空,直接儲存鍵值對 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
get()
獲得當前執行緒中儲存的以ThreadLocal物件為鍵的鍵值對的值。首先獲取當前執行緒關聯的ThreadLocalMap,再獲得以當前ThreadLocal物件為鍵的鍵值對,map為空的話返回初始值null,即執行緒區域性變數為null,
public T get() { //獲取與當前執行緒繫結的ThreadLocalMap Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //map不為空,獲取鍵值對物件 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } private Entry getEntry(ThreadLocal<?> key) { //雜湊 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //判斷雜湊位置的鍵值對是否符合條件:e.get()==key if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } //線性探測尋找key對應的鍵值對 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; }
remove()
從ThreadLocalMap中移除鍵值對,一般在get方法取出儲存的執行緒區域性變數後呼叫remove方法防止記憶體洩露。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
為什麼ThreadLocalMap的鍵是WeakReferrence?
鍵值對物件Enry的鍵是ThreadLocal物件,但是使用WeakReferrence虛引用包裝了的,虛引用相對於我們經常使用的 String str = "abc"
這種強引用來說對GC回收物件的影響較小,以下是虛引用的介紹:
WeakReference是Java語言規範中為了區別直接的物件引用(程式中通過建構函式宣告出來的物件引用)而定義的另外一種引用關係。WeakReference標誌性的特點是:reference例項不會影響到被應用物件的GC回收行為(即只要物件被除WeakReference物件之外所有的物件解除引用後,該物件便可以被GC回收),只不過在被物件回收之後,reference例項想獲得被應用的物件時程式會返回null。
如果Entry的鍵使用強引用,那麼我們存入的鍵值對即使執行緒之後不再使用也不會被回收,生命週期將變得和執行緒的生命週期一樣。而使用了虛引用之後,作為鍵的虛引用並不影響ThreadLocal物件被GC回收,當ThreadLocal物件被回收後,鍵值對就會被標記為 stale entry (過期的鍵值對),再下一次呼叫set/get/remove方法後會進行 ThreadLocalMap層面對過期鍵值對進行回收,防止發生記憶體洩漏。
注意:當我們使用了set方法存入區域性變數後,如果不進行get/remove,那麼過期的鍵值對無法被回收,所以建議在get取出儲存變數後手動remove,可以有效防止記憶體洩漏。
總結
ThreadLocal實現了儲存執行緒區域性變數,ThreadLocal的實現並不是HashMap<Thread,Object>以執行緒物件為鍵,而是線上程內部關聯了一個ThreadLocalMap用於儲存鍵值對,鍵值對的鍵是ThreadLocal物件,所以ThreadLocal物件本身是不儲存內容的,而是作為鍵與儲存內容構成鍵值對。