您現在的位置是:首頁 > 攝影首頁攝影

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

由 Java架構師丨蘇先生 發表于 攝影2021-07-06
簡介這裡提前說一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的雜湊求Key下標的規則:雜湊演算法:keyIndex = ((i + 1) * HASH_INC

黃金分割比例是根號多少

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

一. 前提

最近接觸到的一個專案要相容新老系統,最終採用了ThreadLocal(實際上用的是InheritableThreadLocal)用於在子執行緒獲取父執行緒中共享的變數。問題是解決了,但是後來發現對ThreadLocal的理解不夠深入,於是順便把它的原始碼閱讀理解了一遍。在談到ThreadLocal之前先買個關子,先談談黃金分割數。本文在閱讀ThreadLocal原始碼的時候是使用JDK8(1。8。0_181)。

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

二. 黃金分割數與斐波那契數列

首先複習一下斐波那契數列,下面的推導過程來自某搜尋引擎的wiki:

斐波那契數列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

通項公式:假設F(n)為該數列的第n項(n ∈ N*),那麼這句話可以寫成如下形式:F(n) = F(n-1) + F(n-2)。

有趣的是,這樣一個完全是自然數的數列,通項公式卻是用無理數來表達的。而且當n趨向於無窮大時,前一項與後一項的比值越來越逼近0。618(或者說後一項與前一項的比值小數部分越來越逼近0。618),而這個值0。618就被稱為黃金分割數。證明過程如下:

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

黃金分割數的準確值為(根號5 - 1)/2,約等於0。618。

三. 黃金分割數的應用

黃金分割數被廣泛使用在美術、攝影等藝術領域,因為它具有嚴格的比例性、藝術性、和諧性,蘊藏著豐富的美學價值,能夠激發人的美感。當然,這些不是本文研究的方向,我們先嚐試求出無符號整型和帶符號整型的黃金分割數的具體值:

public static void main(String[] args) throws Exception { //黃金分割數 * 2的32次方 = 2654435769 - 這個是無符號32位整數的黃金分割數對應的那個值 long c = (long) ((1L << 32) * (Math。sqrt(5) - 1) / 2); System。out。println(c); //強制轉換為帶符號為的32位整型,值為-1640531527 int i = (int) c; System。out。println(i);}

透過一個線段圖理解一下:

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

也就是2654435769為32位無符號整數的黃金分割值,而-1640531527就是32位帶符號整數的黃金分割值。而ThreadLocal中的雜湊魔數正是1640531527(十六進位制為0x61c88647)。為什麼要使用0x61c88647作為雜湊魔數?這裡提前說一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的雜湊求Key下標的規則:

雜湊演算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)

其中,i為ThreadLocal例項的個數,這裡的HASH_INCREMENT就是雜湊魔數0x61c88647,length為ThreadLocalMap中可容納的Entry(K-V結構)的個數(或者稱為容量)。在ThreadLocal中的內部類ThreadLocalMap的初始化容量為16,擴容後總是2的冪次方,因此我們可以寫個Demo模擬整個雜湊的過程:

public class Main { private static final int HASH_INCREMENT = 0x61c88647; public static void main(String[] args) throws Exception { hashCode(4); hashCode(16); hashCode(32); } private static void hashCode(int capacity) throws Exception { int keyIndex; for (int i = 0; i < capacity; i++) { keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1); System。out。print(keyIndex); System。out。print(“ ”); } System。out。println(); }}

上面的例子中,我們分別模擬了ThreadLocalMap容量為4,16,32的情況下,不觸發擴容,並且分別”放入”4,16,32個元素到容器中,輸出結果如下:

3 2 1 0 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0

每組的元素經過雜湊演算法後恰好填充滿了整個容器,也就是實現了完美雜湊。實際上,這個並不是偶然,其實整個雜湊演算法可以轉換為多項式證明:證明(x - y) * HASH_INCREMENT != 2^n * (n m),在x != y,n != m,HASH_INCREMENT為奇數的情況下恆成立,具體證明可以自行完成。HASH_INCREMENT賦值為0x61c88647的API文件註釋如下:

連續生成的雜湊碼之間的差異(增量值),將隱式順序執行緒本地id轉換為幾乎最佳分佈的乘法雜湊值,這些不同的雜湊值最終生成一個2的冪次方的雜湊表。

四. ThreadLocal是什麼

下面引用ThreadLocal的API註釋:

This class provides thread-local variables。 These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable。 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例項的時候(透過其get或set方法)都有自己的、獨立初始化的變數副本。ThreadLocal例項通常是類中的私有靜態欄位,使用它的目的是希望將狀態(例如,使用者ID或事務ID)與執行緒關聯起來。

ThreadLocal由Java界的兩個大師級的作者編寫,Josh Bloch和Doug Lea。Josh Bloch是JDK5語言增強、Java集合(Collection)框架的創辦人以及《Effective Java》系列的作者。Doug Lea是JUC(java。util。concurrent)包的作者,Java併發程式設計的泰斗。所以,ThreadLocal的原始碼十分值得學習。

五. ThreadLocal的原理

ThreadLocal雖然叫執行緒本地(區域性)變數,但是實際上它並不存放任何的資訊,可以這樣理解:它是執行緒(Thread)操作ThreadLocalMap中存放的變數的橋樑。它主要提供了初始化、set()、get()、remove()幾個方法。這樣說可能有點抽象,下面畫個圖說明一下線上程中使用ThreadLocal例項的set()和get()方法的簡單流程圖。

假設我們有如下的程式碼,主執行緒的執行緒名字是main(也有可能不是main):

public class Main { private static final ThreadLocal LOCAL = new ThreadLocal<>(); public static void main(String[] args) throws Exception{ LOCAL。set(“doge”); System。out。println(LOCAL。get()); }}

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

上面只描述了單執行緒的情況並且因為是主執行緒忽略了Thread t = new Thread()這一步,如果有多個執行緒會稍微複雜一些,但是原理是不變的,ThreadLocal例項總是透過Thread。currentThread()獲取到當前操作執行緒例項,然後去操作執行緒例項中的ThreadLocalMap型別的成員變數,因此它是一個橋樑,本身不具備儲存功能。

六. ThreadLocal原始碼分析

對於ThreadLocal的原始碼,我們需要重點關注set()、get()、remove()幾個方法。

1. ThreadLocal的內部屬性

//獲取下一個ThreadLocal例項的雜湊魔數private final int threadLocalHashCode = nextHashCode();//原子計數器,主要到它被定義為靜態private static AtomicInteger nextHashCode = new AtomicInteger();//雜湊魔數(增長數),也是帶符號的32位整型值黃金分割值的取正private static final int HASH_INCREMENT = 0x61c88647;//生成下一個雜湊魔數private static int nextHashCode() { return nextHashCode。getAndAdd(HASH_INCREMENT);}

這裡需要注意一點,threadLocalHashCode是一個final的屬性,而原子計數器變數nextHashCode和生成下一個雜湊魔數的方法nextHashCode()是靜態變數和靜態方法,靜態變數只會初始化一次。換而言之,每新建一個ThreadLocal例項,它內部的threadLocalHashCode就會增加0x61c88647。舉個例子:

//t1中的threadLocalHashCode變數為0x61c88647ThreadLocal t1 = new ThreadLocal();//t2中的threadLocalHashCode變數為0x61c88647 + 0x61c88647ThreadLocal t2 = new ThreadLocal();//t3中的threadLocalHashCode變數為0x61c88647 + 0x61c88647 + 0x61c88647ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode是下面的ThreadLocalMap結構中使用的雜湊演算法的核心變數,對於每個ThreadLocal例項,它的threadLocalHashCode是唯一的。

2. 內部類ThreadLocalMap的基本結構和原始碼分析

ThreadLocal內部類ThreadLocalMap使用了預設修飾符,也就是包(包私有)可訪問的。ThreadLocalMap內部定義了一個靜態類Entry。我們重點看下ThreadLocalMap的原始碼,先看成員和結構部分:

/** * ThreadLocalMap是一個定製的雜湊對映,僅適用於維護執行緒本地變數。 * 它的所有方法都是定義在ThreadLocal類之內。 * 它是包私有的,所以在Thread類中可以定義ThreadLocalMap作為變數。 * 為了處理非常大(指的是值)和長時間的用途,雜湊表的Key使用了弱引用(WeakReferences)。 * 引用的佇列(弱引用)不再被使用的時候,對應的過期的條目就能透過主動刪除移出雜湊表。 */static class ThreadLocalMap { //注意這裡的Entry的Key為WeakReference> static class Entry extends WeakReference> { //這個是真正的存放的值 Object value; // Entry的Key就是ThreadLocal例項本身,Value就是輸入的值 Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //初始化容量,必須是2的冪次方 private static final int INITIAL_CAPACITY = 16; //雜湊(Entry)表,必須時擴容,長度必須為2的冪次方 private Entry[] table; //雜湊表中元素(Entry)的個數 private int size = 0; //下一次需要擴容的閾值,預設值為0 private int threshold; //設定下一次需要擴容的閾值,設定值為輸入值len的三分之二 private void setThreshold(int len) { threshold = len * 2 / 3; } // 以len為模增加i private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } // 以len為模減少i private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }}

這裡注意到十分重要的一點:ThreadLocalMap$Entry是WeakReference(弱引用),並且鍵值Key為ThreadLocal<?>例項本身,這裡使用了無限定的泛型萬用字元。

接著看ThreadLocalMap的建構函式:

// 構造ThreadLocal時候使用,對應ThreadLocal的例項方法void createMap(Thread t, T firstValue)ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // 雜湊表預設容量為16 table = new Entry[INITIAL_CAPACITY]; // 計算第一個元素的雜湊碼 int i = firstKey。threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY);}// 構造InheritableThreadLocal時候使用,基於父執行緒的ThreadLocalMap裡面的內容進行提取放入新的ThreadLocalMap的雜湊表中// 對應ThreadLocal的靜態方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap。table; int len = parentTable。length; setThreshold(len); table = new Entry[len]; // 基於父ThreadLocalMap的雜湊表進行複製 for (Entry e : parentTable) { if (e != null) { @SuppressWarnings(“unchecked”) ThreadLocal key = (ThreadLocal) e。get(); if (key != null) { Object value = key。childValue(e。value); Entry c = new Entry(key, value); int h = key。threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } }}

這裡注意一下,ThreadLocal的set()方法呼叫的時候會懶初始化一個ThreadLocalMap並且放入第一個元素。而ThreadLocalMap的私有構造是提供給靜態方法ThreadLocal#createInheritedMap()使用的。

接著看ThreadLocalMap提供給ThreadLocal使用的一些例項方法:

// 如果Key在雜湊表中找不到雜湊槽的時候會呼叫此方法private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab。length; // 這裡會透過nextIndex嘗試遍歷整個雜湊表,如果找到匹配的Key則返回Entry // 如果雜湊表中存在Key == null的情況,呼叫expungeStaleEntry進行清理 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;}// 1。清空staleSlot對應雜湊槽的Key和Value// 2。對staleSlot到下一個空的雜湊槽之間的所有可能衝突的雜湊表部分槽進行重雜湊,置空Key為null的槽// 3。注意返回值是staleSlot之後的下一個空的雜湊槽的雜湊碼private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab。length; // expunge entry at staleSlot // 清空staleSlot對應雜湊槽的Key和Value tab[staleSlot]。value = null; tab[staleSlot] = null; size——; // Rehash until we encounter null // 下面的過程是對staleSlot到下一個空的雜湊槽之間的所有可能衝突的雜湊表部分槽進行重雜湊,置空Key為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) { e。value = null; tab[i] = null; size——; } else { int h = k。threadLocalHashCode & (len - 1); if (h != i) { 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;}// 這裡個方法比較長,作用是替換雜湊碼為staleSlot的雜湊槽中Entry的值private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab。length; Entry e; // Back up to check for prior stale entry in current run。 // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i。e。, whenever the collector runs)。 int slotToExpunge = staleSlot; // 這個迴圈主要是為了找到staleSlot之前的最前面的一個Key為null的雜湊槽的雜湊碼 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e。get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first // 遍歷staleSlot之後的雜湊槽,如果Key匹配則用輸入值替換 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e。get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order。 // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run。 if (k == key) { e。value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn‘t find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run。 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // Key匹配不了,則新建立一個雜湊槽 // If key not found, put new entry in stale slot tab[staleSlot]。value = null; tab[staleSlot] = new Entry(key, value); // 這裡如果當前的staleSlot和找到前置的slotToExpunge不一致會進行一次清理 // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}// 對當前雜湊表中所有的Key為null的Entry呼叫expungeStaleEntryprivate void expungeStaleEntries() { Entry[] tab = table; int len = tab。length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e。get() == null) expungeStaleEntry(j); }}// 清理第i個雜湊槽之後的n個雜湊槽,如果遍歷的時候發現Entry的Key為null,則n會重置為雜湊表的長度,expungeStaleEntry有可能會重雜湊使得雜湊表長度發生變化private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab。length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e。get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed;}/** * 這個方法主要給`ThreadLocal#get()`呼叫,通過當前ThreadLocal例項獲取雜湊表中對應的Entry * */private Entry getEntry(ThreadLocal<?> key) { // 計算Entry的雜湊值 int i = key。threadLocalHashCode & (table。length - 1); Entry e = table[i]; if (e != null && e。get() == key) return e; else // 注意這裡,如果e為null或者Key對不上,會呼叫getEntryAfterMiss return getEntryAfterMiss(key, i, e);}// 重雜湊,必要時進行擴容private void rehash() { // 清理所有空的雜湊槽,並且進行重雜湊 expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis // 雜湊表的雜湊元素個數大於3/4閾值時候觸發擴容 if (size >= threshold - threshold / 4) resize();}// 擴容,簡單的擴大2倍的容量 private void resize() { Entry[] oldTab = table; int oldLen = oldTab。length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (Entry e : oldTab) { if (e != null) { ThreadLocal<?> k = e。get(); if (k == null) { e。value = null; // Help the GC } else { int h = k。threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab;}// 基於ThreadLocal作為key,對當前的雜湊表設定值,此方法由`ThreadLocal#set()`呼叫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(); // Key匹配,直接設定值 if (k == key) { e。value = value; return; } // 如果Entry的Key為null,則替換該Key為當前的key,並且設定值 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; // 清理當前新設定元素的雜湊槽下標到sz段的雜湊槽,如果清理成功並且sz大於閾值則觸發擴容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}

簡單來說,ThreadLocalMap是ThreadLocal真正的資料儲存容器,實際上ThreadLocal資料操作的複雜部分的所有邏輯都在ThreadLocalMap中進行,而ThreadLocalMap例項是Thread的成員變數,在ThreadLocal#set()方法首次呼叫的時候設定到當前執行的執行緒例項中。如果在同一個執行緒中使用多個ThreadLocal例項,實際上,每個ThreadLocal例項對應的是ThreadLocalMap的雜湊表中的一個雜湊槽。舉個例子,在主函式主執行緒中使用多個ThreadLocal例項:

public class ThreadLocalMain { private static final ThreadLocal TL_1 = new ThreadLocal<>(); private static final ThreadLocal TL_2 = new ThreadLocal<>(); private static final ThreadLocal TL_3 = new ThreadLocal<>(); public static void main(String[] args) throws Exception { TL_1。set(1); TL_2。set(“1”); TL_3。set(1L); Field field = Thread。class。getDeclaredField(“threadLocals”); field。setAccessible(true); Object o = field。get(Thread。currentThread()); System。out。println(o); }}

實際上,主執行緒的threadLocals屬性中的雜湊表中一般不止我們上面定義的三個ThreadLocal,因為載入主執行緒的時候還有可能在其他地方使用到ThreadLocal,筆者某次Debug的結果如下:

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

用PPT畫圖簡化一下:

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

上圖threadLocalHashCode屬性一行的表是為了標出每個Entry的雜湊槽的雜湊值,實際上,threadLocalHashCode是ThreadLocal@XXXX中的一個屬性,這是很顯然的,本來threadLocalHashCode就是ThreadLocal的一個成員變數。

上面只是簡單粗略對ThreadLocalMap的原始碼進行了流水賬的分析,下文會作一些詳細的圖,說明一下ThreadLocal和ThreadLocalMap中的一些核心操作的過程。

3. ThreadLocal的建立

從ThreadLocal的建構函式來看,ThreadLocal例項的構造並不會做任何操作,只是為了得到一個ThreadLocal的泛型例項,後續可以把它作為ThreadLocalMap$Entry的鍵:

// 注意threadLocalHashCode在每個新`ThreadLocal`例項的構造同時已經確定了private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() { return nextHashCode。getAndAdd(HASH_INCREMENT);}// 透過Supplier去覆蓋initialValue方法public static ThreadLocal withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier);}// 預設公有建構函式public ThreadLocal() {}

注意threadLocalHashCode在每個新ThreadLocal例項的構造同時已經確定了,這個值也是Entry雜湊表的雜湊槽繫結的雜湊值。

4. TreadLocal的set方法

ThreadLocal中set()方法的原始碼如下:

public void set(T value) { //設定值前總是獲取當前執行緒例項 Thread t = Thread。currentThread(); //從當前執行緒例項中獲取threadLocals屬性 ThreadLocalMap map = getMap(t); if (map != null) //threadLocals屬性不為null則覆蓋key為當前的ThreadLocal例項,值為value map。set(this, value); else //threadLocals屬性為null,則建立ThreadLocalMap,第一個項的Key為當前的ThreadLocal例項,值為value createMap(t, value);}// 這裡看到獲取ThreadLocalMap例項時候總是從執行緒例項的成員變數獲取ThreadLocalMap getMap(Thread t) { return t。threadLocals;}// 建立ThreadLocalMap例項的時候,會把新例項賦值到執行緒例項的threadLocals成員void createMap(Thread t, T firstValue) { t。threadLocals = new ThreadLocalMap(this, firstValue);}

上面的過程原始碼很簡單,設定值的時候總是先獲取當前執行緒例項並且操作它的變數threadLocals。步驟是:

獲取當前執行執行緒的例項。

透過執行緒例項獲取執行緒例項成員threadLocals(ThreadLocalMap),如果為null,則建立一個新的ThreadLocalMap例項賦值到threadLocals。

透過threadLocals設定值value,如果原來的雜湊槽已經存在值,則進行覆蓋。

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

5. TreadLocal的get方法

ThreadLocal中get()方法的原始碼如下:

public T get() { //獲取當前執行緒的例項 Thread t = Thread。currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //根據當前的ThreadLocal例項獲取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法 ThreadLocalMap。Entry e = map。getEntry(this); if (e != null) { @SuppressWarnings(“unchecked”) T result = (T) e。value; return result; } } //執行緒例項中的threadLocals為null,則呼叫initialValue方法,並且建立ThreadLocalMap賦值到threadLocals return setInitialValue();}private T setInitialValue() { // 呼叫initialValue方法獲取值 T value = initialValue(); Thread t = Thread。currentThread(); ThreadLocalMap map = getMap(t); // ThreadLocalMap如果未初始化則進行一次建立,已初始化則直接設定值 if (map != null) map。set(this, value); else createMap(t, value); return value;}protected T initialValue() { return null;}

initialValue()方法預設返回null,如果ThreadLocal例項沒有使用過set()方法直接使用get()方法,那麼ThreadLocalMap中的此ThreadLocal為Key的項會把值設定為initialValue()方法的返回值。如果想改變這個邏輯可以對initialValue()方法進行覆蓋。

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

6. TreadLocal的remove方法

ThreadLocal中remove()方法的原始碼如下:

public void remove() { //獲取Thread例項中的ThreadLocalMap ThreadLocalMap m = getMap(Thread。currentThread()); if (m != null) //根據當前ThreadLocal作為Key對ThreadLocalMap的元素進行移除 m。remove(this);}

阿里架構師淺析ThreadLocal原始碼——黃金分割數的使用

七. ThreadLocal.ThreadLocalMap的初始化

我們可以關注一下java。lang。Thread類裡面的變數:

public class Thread implements Runnable { //傳遞ThreadLocal中的ThreadLocalMap變數 ThreadLocal。ThreadLocalMap threadLocals = null; //傳遞InheritableThreadLocal中的ThreadLocalMap變數 ThreadLocal。ThreadLocalMap inheritableThreadLocals = null;}

也就是,ThreadLocal需要存放和獲取的資料實際上繫結在Thread例項的成員變數threadLocals中,並且是ThreadLocal#set()方法呼叫的時候才進行懶載入的,可以結合上一節的內容理解一下,這裡不展開。

八. 什麼情況下ThreadLocal的使用會導致記憶體洩漏

其實ThreadLocal本身不存放任何的資料,而ThreadLocal中的資料實際上是存放線上程例項中,從實際來看是執行緒記憶體洩漏,底層來看是Thread物件中的成員變數threadLocals持有大量的K-V結構,並且執行緒一直處於活躍狀態導致變數threadLocals無法釋放被回收。threadLocals持有大量的K-V結構這一點的前提是要存在大量的ThreadLocal例項的定義,一般來說,一個應用不可能定義大量的ThreadLocal,所以一般的洩漏源是執行緒一直處於活躍狀態導致變數threadLocals無法釋放被回收。但是我們知道,·ThreadLocalMap·中的Entry結構的Key用到了弱引用(·WeakReference>·),當沒有強引用來引用ThreadLocal例項的時候,JVM的GC會回收ThreadLocalMap中的這些Key,此時,ThreadLocalMap中會出現一些Key為null,但是Value不為null的Entry項,這些Entry項如果不主動清理,就會一直駐留在ThreadLocalMap中。也就是為什麼ThreadLocal中get()、set()、remove()這些方法中都存在清理ThreadLocalMap例項key為null的程式碼塊。總結下來,記憶體洩漏可能出現的地方是:

大量地(靜態)初始化ThreadLocal例項,初始化之後不再呼叫get()、set()、remove()方法。

初始化了大量的ThreadLocal,這些ThreadLocal中存放了容量大的Value,並且使用了這些ThreadLocal例項的執行緒一直處於活躍的狀態。

ThreadLocal中一個設計亮點是ThreadLocalMap中的Entry結構的Key用到了弱引用。試想如果使用強引用,等於ThreadLocalMap中的所有資料都是與Thread的生命週期繫結,這樣很容易出現因為大量執行緒持續活躍導致的記憶體洩漏。使用了弱引用的話,JVM觸發GC回收弱引用後,ThreadLocal在下一次呼叫get()、set()、remove()方法就可以刪除那些ThreadLocalMap中Key為null的值,起到了惰性刪除釋放記憶體的作用。

其實ThreadLocal在設定內部類ThreadLocal。ThreadLocalMap中構建的Entry雜湊表已經考慮到記憶體洩漏的問題,所以ThreadLocal。ThreadLocalMap$Entry類設計為弱引用,類簽名為static class Entry extends WeakReference>。之前一篇文章介紹過,如果弱引用關聯的物件如果置為null,那麼該弱引用會在下一次GC時候回收弱引用關聯的物件。舉個例子:

public class ThreadLocalMain { private static ThreadLocal TL_1 = new ThreadLocal<>(); public static void main(String[] args) throws Exception { TL_1。set(1); TL_1 = null; System。gc(); Thread。sleep(300); }}

這種情況下,TL_1這個ThreadLocal在主動GC之後,執行緒繫結的ThreadLocal。ThreadLocalMap例項中的Entry雜湊表中原來的TL_1所在的雜湊槽Entry的引用持有值referent(繼承自WeakReference)會變成null,但是Entry中的value是強引用,還存放著TL_1這個ThreadLocal未回收之前的值。這些被”孤立”的雜湊槽Entry就是前面說到的要惰性刪除的雜湊槽。

九. ThreadLocal的最佳實踐

其實ThreadLocal的最佳實踐很簡單:

每次使用完ThreadLocal例項,都呼叫它的remove()方法,清除Entry中的資料。

呼叫remove()方法最佳時機是執行緒執行結束之前的finally程式碼塊中呼叫,這樣能完全避免操作不當導致的記憶體洩漏,這種主動清理的方式比惰性刪除有效。

十. 小結

ThreadLocal執行緒本地變數是執行緒例項傳遞和儲存共享變數的橋樑,真正的共享變數還是存放線上程例項本身的屬性中。ThreadLocal裡面的基本邏輯並不複雜,但是一旦涉及到效能影響、記憶體回收(弱引用)和惰性刪除等環節,其實它考慮到的東西還是相對全面而且有效的。

寫在最後

第一:看完點贊,感謝您的認可;

...

第二:隨手轉發,分享知識,讓更多人學習到;

...

第三:記得點關注,每天更新的!!!

...

Copyright © 2024看文娛