文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈為小伙伴奉上以下珍貴的學習資源:
- 瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高並發實戰》 面試必備 + 大廠必備 + 漲薪必備
- 瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高並發核心編程》 面試必備 + 大廠必備 + 漲薪必備
- 資源寶庫: Java程序員必備 網盤資源大集合 價值>1000元 隨便取 GO->【博客園總入口 】
- 獨孤九劍:Netty靈魂實驗 : 本地 100W連接 高並發實驗,瞬間提升Java內力
推薦2:史上最全 Java 面試題 21 個專題
史上最全 Java 面試題 21 個專題 | 阿里、京東、美團、頭條.... 隨意挑、橫着走!!! |
---|---|
1: JVM面試題(史上最強、持續更新、吐血推薦) | https://www.cnblogs.com/crazymakercircle/p/14365820.html |
2:Java基礎面試題(史上最全、持續更新、吐血推薦) | https://www.cnblogs.com/crazymakercircle/p/14366081.html |
3:死鎖面試題(史上最強、持續更新) | https://www.cnblogs.com/crazymakercircle/p/14323919.html |
4:設計模式面試題 (史上最全、持續更新、吐血推薦) | https://www.cnblogs.com/crazymakercircle/p/14367101.html |
5:架構設計面試題 (史上最全、持續更新、吐血推薦) | https://www.cnblogs.com/crazymakercircle/p/14367907.html |
還有 10 + 篇必刷、必刷 的面試題 | 更多 ....., 請參見【 瘋狂創客圈 高並發 總目錄 】 |
推薦3: 瘋狂創客圈 高質量 博文
springCloud 高質量 博文 | |
---|---|
nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
springcloud + webflux 高並發實戰 | Webflux(史上最全) |
SpringCloud gateway (史上最全) | spring security (史上最全) |
還有 10 + 篇 必刷、必刷 的高質量 博文 | 更多 ....., 請參見【 瘋狂創客圈 高並發 總目錄 】 |
一、ThreadLocal 介紹:
正如 JDK 注釋中所說的那樣: ThreadLocal 類提供線程局部變量,它通常是私有類中希望將狀態與線程關聯的靜態字段。
簡而言之,就是 ThreadLocal 提供了線程間數據隔離的功能,從它的命名上也能知道這是屬於一個線程的本地變量。也就是說,每個線程都會在 ThreadLocal 中保存一份該線程獨有的數據,所以它是線程安全的。
熟悉 Spring 的同學可能知道 Bean 的作用域(Scope),而 ThreadLocal 的作用域就是線程。
下面通過一個簡單示例來展示一下 ThreadLocal 的特性:
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 創建一個有2個核心線程數的線程池
ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10));
// 線程池提交一個任務,將任務序號及執行該任務的子線程的線程名放到 ThreadLocal 中
threadPool.execute(() -> threadLocal.set("任務1: " + Thread.currentThread().getName()));
threadPool.execute(() -> threadLocal.set("任務2: " + Thread.currentThread().getName()));
threadPool.execute(() -> threadLocal.set("任務3: " + Thread.currentThread().getName()));
// 輸出 ThreadLocal 中的內容
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> System.out.println("ThreadLocal value of " + Thread.currentThread().getName() + " = " + threadLocal.get()));
}
// 線程池記得關閉
threadPool.shutdown();
}
上面代碼首先創建了一個有2個核心線程數的普通線程池,隨后提交一個任務,將任務序號及執行該任務的子線程的線程名放到 ThreadLocal 中,最后在一個 for 循環中輸出線程池中各個線程存儲在 ThreadLocal 中的值。
這個程序的輸出結果是:
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
由此可見,線程池中執行提交的任務的是名為 pool-1-thread-1 的線程,隨后多次輸出線程池核心線程在 ThreadLocal 變量中存儲的的內容也表明:每個線程在 ThreadLocal 中存儲的內容是當前線程獨有的,在多線程環境下,能夠有效防止自己的變量被其他線程修改(存儲的內容是同一個引用類型對象的情況除外)。
二、ThreadLocal 實現原理:
在 JDK1.8 版本中 ThreadLocal 類的源碼總共723行,去掉注釋大概有350行,應該算是 JDK 核心類庫中代碼量比較少的一個類了,相對來說它的源碼還是挺容易理解的。
下面,就從 ThreadLocal 的數據結構開始聊聊它的實現原理吧。
底層數據結構:
ThreadLocal 底層是通過 ThreadLocalMap 這個靜態內部類來存儲數據的,ThreadLocalMap 就是一個鍵值對的 Map,它的底層是 Entry 對象數組,Entry 對象中存放的鍵是 ThreadLocal 對象,值是 Object 類型的具體存儲內容。
除此之外,ThreadLocalMap 也是 Thread 類一個屬性。
如何證明上面給出的 ThreadLocal 類底層數據結構的正確性?
我們可以從 ThreadLocal#get() 方法開始追蹤代碼,看看線程局部變量到底是從哪里被取出來的。
public T get() {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 變量
ThreadLocalMap map = getMap(t);
// 若 threadLocals 變量不為空,根據 ThreadLocal 對象來獲取 key 對應的 value
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 若 threadLocals 變量是 NULL,初始化一個新的 ThreadLocalMap 對象
return setInitialValue();
}
// ThreadLocal#setInitialValue
// 初始化一個新的 ThreadLocalMap 對象
private T setInitialValue() {
// 初始化一個 NULL 值
T value = initialValue();
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 變量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// ThreadLocalMap#createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
通過 ThreadLocal#get() 方法可以很清晰的看到,我們根據 ThreadLocal 對象從 ThreadLocal 中讀取數據時,首先會獲取當前線程對象,然后得到當前線程對象中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 屬性;
如果 threadLocals 屬性不為空,會根據 ThreadLocal 對象作為 key 來獲取 key 對應的 value;如果 threadLocals 變量是 NULL,就初始化一個新的ThreadLocalMap 對象。
再看 ThreadLocalMap 的構造方法,也就是 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 屬性不為空時的執行邏輯。
// 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 對象作為 key,存儲的具體內容 Object 對象作為 value,包裝成一個 Entry 對象,放到 ThreadLocalMap 類中類型為 Entry 數組的 table 屬性中,這樣就完成了線程局部變量的存儲。
所以說, ThreadLocal 中的數據最終是存放在 ThreadLocalMap 這個類中的 。
散列方式:
在 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中我寫了一行注釋:
// 獲取當前 ThreadLocal 對象的散列值
int i = key.threadLocalHashCode & (len-1);
這行代碼得到的值其實是一個 ThreadLocal 對象的散列值,這就是 ThreadLocal 的散列方式,我們稱之為 斐波那契散列 。
// ThreadLocal#threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
// ThreadLocal#nextHashCode
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// ThreadLocal#nextHashCode
private static AtomicInteger nextHashCode = new AtomicInteger();
// AtomicInteger#getAndAdd
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 魔數 ThreadLocal#HASH_INCREMENT
private static final int HASH_INCREMENT = 0x61c88647;
key.threadLocalHashCode 所涉及的函數及屬性如上所示,每一個 ThreadLocal 的 threadLocalHashCode 屬性都是基於魔數 0x61c88647 來生成的。
這里就不討論選擇這個魔數的原因了(其實是我看不太懂),總之大量的實踐證明: 使用 0x61c88647 作為魔數生成的 threadLocalHashCode 再與2的冪取余,得到的結果分布很均勻。
注: 對 A 進行2的冪取余操作 A % 2^N 可以通過 A & (2^n-1) 來代替,位運算的效率比取模效率高很多。
如何解決哈希沖突:
我們已經知道 ThreadLocalMap 類的底層數據結構是一個 Entry 類型的數組,但與 HashMap 中的 Node 類數組+鏈表形式不同的是,Entry 類沒有 next 屬性來構成鏈表,所以它是一個單純的數組。
就算上面所說的 斐波那契散列法 真的能夠充分散列,但難免還是可能會發生哈希碰撞,那么問題來了,Entry 數組是如何解決哈希沖突的?
這就需要拿出 ThreadLocal#set(T value) 方法了,而具體處理哈希沖突的邏輯是在 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中的:
public void set(T value) {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 變量
ThreadLocalMap map = getMap(t);
// 若 threadLocals 變量不為空,進行賦值;否則新建一個 ThreadLocalMap 對象來存儲
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
// 獲取 ThreadLocalMap 的 Entry 數組對象
Entry[] tab = table;
int len = tab.length;
// 基於斐波那契散列法獲取當前 ThreadLocal 對象的散列值
int i = key.threadLocalHashCode & (len-1);
// 解決哈希沖突,線性探測法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 代碼(1)
if (k == key) {
e.value = value;
return;
}
// 代碼(2)
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 代碼(3)將 key-value 包裝成 Entry 對象放在數組退出循環時的位置中
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// ThreadLocalMap#nextIndex
// Entry 數組的下一個索引,若超過數組大小則從0開始,相當於環形數組
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
具體分析處理哈希沖突的 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法,可以看到,在拿到 ThreadLocal 對象的散列值之后進入了一個 for 循環,循環的條件也很清楚:從 Entry 數組的 ThreadLocal 對象散列值處開始,每次向后挪一位,如果超過數組大小則從0開始繼續遍歷,直到 Entry 對象為 NULL 為止。
在循環過程中:
- 如代碼(1),如果當前 ThreadLocal 對象正好等於 Entry 對象中的 key 屬性,直接更新 ThreadLocal 中 value 的值;
- 如代碼(2),如果當前 ThreadLocal 對象不等於 Entry 對象中的 key 屬性,並且 Entry 對象的 key 是空的,這里進行的邏輯其實是 設置鍵值對,同時清理無效的 Entry (一定程序防止內存泄漏,下文會有詳細介紹);
- 如代碼(3),如果在遍歷中沒有發現當前 TheadLocal 對象的散列值,也沒有發現 Entry 對象的 key 為空的情況,而是滿足了退出循環的條件,即 Entry 對象為空時,那么就會創建一個 新的 Entry 對象進行存儲 ,同時做一次 啟發式清理 ,將 Entry 數組中 key 為空,value 不為空的對象的 value 值釋放;
至此,我們分析完了在向 ThreadLocal 中存儲數據時,拿到 ThreadLocal 對象散列值之后的邏輯,回到本小節的主題—— ThreadLocal 是如何解決哈希沖突的?
由上面的代碼可以知道,在基於斐波那契散列法獲取當前 ThreadLocal 對象的散列值之后進入了一個循環,在循環中是處理具體處理哈希沖突的方法:
- 如果散列值已存在且 key 為同一個對象,直接更新 value
- 如果散列值已存在但 key 不是同一個對象,嘗試在下一個空的位置進行存儲
所以,來總結一下 ThreadLocal 處理哈希沖突的方式就是:如果在 set 時遇到哈希沖突,ThreadLocal 會通過線性探測法嘗試在數組下一個索引位置進行存儲,同時在 set 過程中 ThreadLocal 會釋放 key 為 NULL,value 不為 NULL 的臟 Entry對象的 value 屬性來防止內存泄漏 。
初始容量及擴容機制:
在上文中有提到過 ThreadLocalMap 的構造方法,這里詳細說明一下。
// ThreadLocalMap 構造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 Entry 數組
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 設置擴容條件
setThreshold(INITIAL_CAPACITY);
}
ThreadLocalMap 的初始容量是 16:
// 初始化容量
private static final int INITIAL_CAPACITY = 16;
下面聊一下 ThreadLocalMap 的擴容機制 ,它在擴容前有兩個判斷的步驟,都滿足后才會進行最終擴容:
- ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中可能會觸發啟發式清理,在清理無效 Entry 對象后,如果數組長度大於等於數組定義長度的 2/3,則首先進行 rehash;
// rehash 條件
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
- rehash 會觸發一次全量清理,如果數組長度大於等於數組定義長度的 1/2,則進行 resize(擴容);
// 擴容條件
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
- 進行擴容時,Entry 數組為擴容為 原來的2倍 ,重新計算 key 的散列值,如果遇到 key 為 NULL 的情況,會將其 value 也置為 NULL,幫助虛擬機進行GC。
// 具體的擴容函數
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
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 中存儲的是線程的局部變量,那如果現在有個需求,想要實現線程間局部變量傳遞,這該如何實現呢?
大佬們早已料到會有這樣的需求,於是設計出了 InheritableThreadLocal 類。
InheritableThreadLocal 類的源碼除去注釋之外一共不超過10行,因為它是繼承於 ThreadLocal 類,很多東西在 ThreadLocal 類中已經實現了,InheritableThreadLocal 類只重寫了其中三個方法:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
我們先用一個簡單的示例來實踐一下父子線程間局部變量的傳遞功能。
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("這是父線程設置的值");
new Thread(() -> System.out.println("子線程輸出:" + threadLocal.get())).start();
}
// 輸出內容
子線程輸出:這是父線程設置的值
可以看到,在子線程中通過調用 InheritableThreadLocal#get() 方法,拿到了在父線程中設置的值。
那么,這是如何實現的呢?
實現父子線程間的局部變量共享需要追溯到 Thread 對象的構造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
// 該參數一般默認是 true
boolean inheritThreadLocals) {
// 省略大部分代碼
Thread parent = currentThread();
// 復制父線程的 inheritableThreadLocals 屬性,實現父子線程局部變量共享
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// 省略部分代碼
}
在最終執行的構造方法中,有這樣一個判斷:如果當前父線程(創建子線程的線程)的 inheritableThreadLocals 屬性不為 NULL,就會將當下父線程的 inheritableThreadLocals 屬性復制給子線程的 inheritableThreadLocals 屬性。具體的復制方法如下:
// ThreadLocal#createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 一個個復制父線程 ThreadLocalMap 中的數據
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// childValue 方法調用的是 InheritableThreadLocal#childValue(T parentValue)
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++;
}
}
}
}
需要注意的是,復制父線程共享變量的時機是在創建子線程時,如果在創建子線程后父線程再往 InheritableThreadLocal 類型的對象中設置內容,將不再對子線程可見。
ThreadLocal 內存泄漏分析:
最后再來說說 ThreadLocal 的內存泄漏問題,眾所周知,如果使用不當,ThreadLocal 會導致內存泄漏。
內存泄漏 是指程序中已動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。
發生內存泄漏的原因:
而 ThreadLocal 發生內存泄漏的原因需要從 Entry 對象說起。
// ThreadLocal->ThreadLocalMap->Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry 對象的 key 即 ThreadLocal 類是繼承於 WeakReference 弱引用類。具有弱引用的對象有更短暫的生命周期,在發生 GC 活動時,無論內存空間是否足夠,垃圾回收器都會回收具有弱引用的對象。
由於 Entry 對象的 key 是繼承於 WeakReference 弱引用類的,若 ThreadLocal 類沒有外部強引用,當發生 GC 活動時就會將 ThreadLocal 對象回收。
而此時如果創建 ThreadLocal 類的線程依然活動,那么 Entry 對象中 ThreadLocal 對象對應的 value 就依舊具有強引用而不會被回收,從而導致內存泄漏。
如何解決內存泄漏問題:
要想解決內存泄漏問題其實很簡單,只需要記得在使用完 ThreadLocal 中存儲的內容后將它 remove 掉就可以了。
這是主動防止發生內存泄漏問題的手段,但其實設計 ThreadLocal 的大神當然也發現了 ThreadLocal 可能引發內存泄漏的問題,所以他們也設計了相應的手段來防止內存泄漏。
ThreadLocal 內部如何防止內存泄漏:
在上文中描述 ThreadLocalMap#set(ThreadLocal key, Object value) 其實已經有涉及 ThreadLocal 內部清理無效 Entry 的邏輯了,在通過線性檢測法處理哈希沖突時,若 Entry 數組的 key 與當前 ThreadLocal 不是同一個對象,同時 key 為空的時候,會進行 清理無效 Entry 的處理,即 ThreadLOcalMap#replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) 方法:
- 這個方法中也是一個循環,循環的邏輯與 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法一致;
- 在循環過程中如果找到了將要存儲的 ThreadLocal 對象,則會將它與進入 replaceStaleEntry 方法時滿足條件的 k 值做交換,同時將 value 更新;
- 如果沒有找到將要存儲的 ThreadLocal 對象,則會在此 k 值處新建一個 Entry 對象存儲;
- 同時,在循環過程中如果發現其他無效的 Entry( key 為 NULL,value還在的情況,可能導致內存泄漏,下文會有詳細描述),會順勢找到 Entry 數組中所有的無效 Entry,釋放這些無效 Entry(通過將 key 和 value 都設置為NULL),在一定程度上避免了內存泄漏;
如果滿足線性檢測循環結束條件了,即遇到了 Entry==NULL 的情況,就新建一個 Entry 對象來存儲數據。然后會進行一次啟發式清理,如果啟發式清理沒有成功釋放滿足條件的對象,同時滿足擴容條件時,會執行 ThreadLocalMap#rehash() 方法。
private void rehash() {
// 全量清理
expungeStaleEntries();
// 滿足條件則擴容
if (size >= threshold - threshold / 4)
resize();
}
ThreadLocalMap#rehash() 方法中會對 ThreadLocalMap 進行一次全量清理,全量清理會遍歷整個 Entry 數組,刪除所有 key 為 NULL,value 不為 NULL 的臟 Entry對象。
// 全量清理
private 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);
}
}
進行全量清理之后,如果 Entry 數組的大小大於等於 threshold - threshold / 4 ,則會進行2倍擴容。
總結一下:在ThreadLocal 內部是通過在 get、set、remove 方法中主動進行清理 key 為 NULL 且 value 不為 NULL 的無效 Entry 來避免內存泄漏問題。
但是基於 get、set 方法讓 ThreadLocal 自行清理無效 Entry 對象並不能完全避免內存泄漏問題,要徹底解決內存泄漏問題還得養成使用完就主動調用 remove 方法釋放資源的好習慣。
三、ThreadLocal的常見面試題目
什么是ThreadLocal
ThreadLocal 是 JDK java.lang 包下的一個類,是天然的線程安全的類,
1.ThreadLoca 是線程局部變量,這個變量與普通變量的區別,在於每個訪問該變量的線程,在線程內部都會
初始化一個獨立的變量副本,只有該線程可以訪問【get() or set()】該變量,ThreadLocal實例通常聲明
為 private static。
2.線程在存活並且ThreadLocal實例可被訪問時,每個線程隱含持有一個線程局部變量副本,當線程生命周期
結束時,ThreadLocal的實例的副本跟着線程一起消失,被GC垃圾回收(除非存在對這些副本的其他引用)
JDK 源碼中解析:
/**
* 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實例的時候(通過其get或set方法)都有自己的、獨立初始化的變量副本。ThreadLocal實例通常是類中的私有靜態字段,使用它的目的是希望將狀態(例如,用戶ID或事務ID)與線程關聯起來。
ThreadLocalMap 和HashMap區別
HashMap 的數據結構是數組+鏈表
ThreadLocalMap的數據結構僅僅是數組
HashMap 是通過鏈地址法解決hash 沖突的問題
ThreadLocalMap 是通過開放地址法來解決hash 沖突的問題
HashMap 里面的Entry 內部類的引用都是強引用
ThreadLocalMap里面的Entry 內部類中的key 是弱引用,value 是強引用
ThreadLocal怎么用
討論ThreadLocal用在什么地方前,我們先明確下,如果僅僅就一個線程,那么都不用談ThreadLocal的,ThreadLocal是用在多線程的場景的!!!
ThreadLocal歸納下來就3類用途:
- 保存線程上下文信息,在任意需要的地方可以獲取!!!
- 線程安全的,避免某些情況需要考慮線程安全必須同步帶來的性能損失!!!
- 線程間數據隔離
1.保存線程上下文信息,在任意需要的地方可以獲取!!!
由於ThreadLocal的特性,同一線程在某地方進行設置,在隨后的任意地方都可以獲取到。從而可以用來保存線程上下文信息。
常用的比如每個請求怎么把一串后續關聯起來,就可以用ThreadLocal進行set,在后續的任意需要記錄日志的方法里面進行get獲取到請求id,從而把整個請求串起來。
還有比如Spring的事務管理,用ThreadLocal存儲Connection,從而各個DAO可以獲取同一Connection,可以進行事務回滾,提交等操作。
2.線程安全的,避免某些情況需要考慮線程安全必須同步帶來的性能損失!!!
由於不需要共享信息,自然就不存在競爭問題了,從而保證了某些情況下線程的安全,以及避免了某些情況需要考慮線程安全必須同步帶來的性能損失!!!
ThreadLocal局限性
ThreadLocal為解決多線程程序的並發問題提供了一種新的思路。但是ThreadLocal也有局限性,我們來看看阿里規范:
這類場景阿里規范里面也提到了:
ThreadLocal用法
public class MyThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) throws InterruptedException {
int threads = 9;
MyThreadLocalDemo demo = new MyThreadLocalDemo();
CountDownLatch countDownLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
threadLocal.set(Thread.currentThread().getName());
System.out.println("threadLocal.get()================>" + threadLocal.get());
countDownLatch.countDown();
}, "執行線程 - " + i);
thread.start();
}
countDownLatch.await();
}
}
代碼運行結果:
threadLocal.get()================>執行線程 - 1
threadLocal.get()================>執行線程 - 0
threadLocal.get()================>執行線程 - 3
threadLocal.get()================>執行線程 - 4
threadLocal.get()================>執行線程 - 5
threadLocal.get()================>執行線程 - 8
threadLocal.get()================>執行線程 - 7
threadLocal.get()================>執行線程 - 2
threadLocal.get()================>執行線程 - 6
Process finished with exit code 0
ThreadLocal的原理
ThreadLocal雖然叫線程局部變量,但是實際上它並不存放任何的信息,可以這樣理解:它是線程(Thread)操作ThreadLocalMap中存放的變量的橋梁。它主要提供了初始化、set()、get()、remove()幾個方法。這樣說可能有點抽象,下面畫個圖說明一下在線程中使用ThreadLocal實例的set()和get()方法的簡單流程圖。
假設我們有如下的代碼,主線程的線程名字是main(也有可能不是main):
public class Main {
private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();
public static void main(String[] args) throws Exception{
LOCAL.set("doge");
System.out.println(LOCAL.get());
}
}
上面只描述了單線程的情況並且因為是主線程忽略了Thread t = new Thread()這一步,如果有多個線程會稍微復雜一些,但是原理是不變的,ThreadLocal實例總是通過Thread.currentThread()獲取到當前操作線程實例,然后去操作線程實例中的ThreadLocalMap類型的成員變量,因此它是一個橋梁,本身不具備存儲功能
鏈地址法
這種方法的基本思想是將所有哈希地址為i的元素構成一個稱為同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第i個單元中,因而查找、插入和刪除主要在同義詞鏈中進行。列如對於關鍵字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我們用前面同樣的12為除數,進行除留余數法:
開放地址法
這種方法的基本思想是一旦發生了沖突,就去尋找下一個空的散列地址(這非常重要,源碼都是根據這個特性,必須理解這里才能往下走),只要散列表足夠大,空的散列地址總能找到,並將記錄存入。
比如說,我們的關鍵字集合為{12,33,4,5,15,25},表長為10。 我們用散列函數f(key) = key mod l0。 當計算前S個數{12,33,4,5}時,都是沒有沖突的散列地址,直接存入(藍色代表為空的,可以存放數據):
計算key = 15時,發現f(15) = 5,此時就與5所在的位置沖突。於是我們應用上面的公式f(15) = (f(15)+1) mod 10 =6。於是將15存入下標為6的位置。這其實就是房子被人買了於是買下一間的作法:
鏈地址法和開放地址法的優缺點
開放地址法:
容易產生堆積問題,不適於大規模的數據存儲。
散列函數的設計對沖突會有很大的影響,插入時可能會出現多次沖突的現象。
刪除的元素是多個沖突元素中的一個,需要對后面的元素作處理,實現較復雜。
鏈地址法:
處理沖突簡單,且無堆積現象,平均查找長度短。
鏈表中的結點是動態申請的,適合構造表不能確定長度的情況。
刪除結點的操作易於實現。只要簡單地刪去鏈表上相應的結點即可。
指針需要額外的空間,故當結點規模較小時,開放定址法較為節省空間。
ThreadLocalMap 采用開放地址法原因
ThreadLocal 中看到一個屬性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一個神奇的數字,讓哈希碼能均勻的分布在2的N次方的數組里, 即 Entry[] table
通過HASH_INCREMENT 可以看到,ThreadLocal
中使用了斐波那契散列法,來保證哈希表的離散度。而它選用的乘數值即是2^32 * 黃金分割比
。
什么是散列?
散列(Hash)也稱為哈希,就是把任意長度的輸入,通過散列算法,變換成固定長度的輸出,這個輸出值就是散列值。
ThreadLocal 往往存放的數據量不會特別大(而且key 是弱引用又會被垃圾回收,及時讓數據量更小),這個時候開放地址法簡單的結構會顯得更省空間,同時數組的查詢效率也是非常高,加上第一點的保障,沖突概率也低.
解決哈希沖突
ThreadLocal中的hash code非常簡單,就是調用AtomicInteger的getAndAdd方法,參數是個固定值0x61c88647。
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
上面說過ThreadLocalMap的結構非常簡單只用一個數組存儲,並沒有鏈表結構,當出現Hash沖突時采用線性查找的方式,所謂線性查找,就是根據初始key的hashcode值確定元素在table數組中的位置,如果發現這個位置上已經有其他key值的元素被占用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。如果產生多次hash沖突,處理起來就沒有HashMap的效率高,為了避免哈希沖突,使用盡量少的threadlocal變量
內存泄漏問題
在JAVA里面,存在強引用、弱引用、軟引用、虛引用。這里主要談一下強引用和弱引用。
強引用,就不必說了,類似於:
A a = new A();
B b = new B();
考慮這樣的情況:
C c = new C(b);
b = null;
考慮下GC的情況。要知道b被置為null,那么是否意味着一段時間后GC工作可以回收b所分配的內存空間呢?答案是否定的,因為即便b被置為null,但是c仍然持有對b的引用,而且還是強引用,所以GC不會回收b原先所分配的空間!既不能回收利用,又不能使用,這就造成了內存泄露。
那么如何處理呢?
可以c = null;也可以使用弱引用!(WeakReference w = new WeakReference(b);)
ThreadLocal使用到了弱引用,是否意味着不會存在內存泄露呢?
把ThreadLocal置為null,那么意味着Heap中的ThreadLocal實例不在有強引用指向,只有弱引用存在,因此GC是可以回收這部分空間的,也就是key是可以回收的。但是value卻存在一條從Current Thread過來的強引用鏈。因此只有當Current Thread銷毀時,value才能得到釋放。
只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設為null和線程結束這段時間內不會被回收的,就發生了我們認為的內存泄露。最要命的是線程對象不被回收的情況,比如使用線程池的時候,線程結束是不會銷毀的,再次使用的,就可能出現內存泄露。
那么如何有效的避免呢?
在ThreadLocalMap中的set/getEntry方法中,會對key為null(也即是ThreadLocal為null)進行判斷,如果為null的話,那么是會對value置為null的。我們也可以通過調用ThreadLocal的remove方法進行釋放!也就是每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
ThreadLocal使用
ThreadLocal使用的一般步驟:
1、在多線程的類(如ThreadDemo類)中。創建一個ThreadLocal對象threadXxx,用來保存線程間須要隔離處理的對象xxx。
2、在ThreadDemo類中。創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中推斷,若ThreadLocal對象為null時候,應該new()一個隔離訪問類型的對象,並強制轉換為要應用的類型。
3、在ThreadDemo類的run()方法中。通過getXxx()方法獲取要操作的數據。這樣能夠保證每一個線程相應一個數據對象,在不論什么時刻都操作的是這個對象。
使用示例:
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
threadLocal.set(i);
System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal.remove();
}
}, "threadLocal test 1").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal.remove();
}
}, "threadLocal test 2").start();
}
輸出
threadLocal test 1 = 0
threadLocal test 2 = null
threadLocal test 2 = null
threadLocal test 1 = 1
threadLocal test 2 = null
threadLocal test 1 = 2
threadLocal test 2 = null
threadLocal test 1 = 3
threadLocal test 2 = null
threadLocal test 1 = 4
threadLocal test 2 = null
threadLocal test 1 = 5
threadLocal test 2 = null
threadLocal test 1 = 6
threadLocal test 2 = null
threadLocal test 1 = 7
threadLocal test 2 = null
threadLocal test 1 = 8
threadLocal test 2 = null
threadLocal test 1 = 9
與Synchonized的對照:
ThreadLocal和Synchonized都用於解決多線程並發訪問。可是ThreadLocal與synchronized有本質的差別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該僅僅能被一個線程訪問。而ThreadLocal為每個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並非同一個對象,這樣就隔離了多個線程對數據的數據共享。而Synchronized卻正好相反,它用於在多個線程間通信時可以獲得數據共享。
Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。
線程隔離特性
線程隔離特性,只有在線程內才能獲取到對應的值,線程外不能訪問。
(1)Synchronized是通過線程等待,犧牲時間來解決訪問沖突
(1)ThreadLocal是通過每個線程單獨一份存儲空間,犧牲空間來解決沖突
需要了解ThreadLocal的源碼解析: 點此了解
四、ThreadLocal源碼分析
從Thread源碼入手:
public class Thread implements Runnable {
......
//與此線程有關的ThreadLocal值。該映射由ThreadLocal類維護。
ThreadLocal.ThreadLocalMap threadLocals = null;
//與此線程有關的InheritableThreadLocal值。該Map由InheritableThreadLocal類維護
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}
從上面Thread類源代碼可以看出Thread類中有一個threadLocals和一個inheritableThreadLocals 變量,它們都是ThreadLocalMap類型的變量,默認情況下這兩個變量都是null,只有當前線程調用ThreadLocal類的Iset或get方法時才創建它們,實際上調用這兩個方法的時候,我們調用的是ThreadLocalMap類對應的get()、set()方法。
1.ThreadLocal的內部屬性
ThreadLocalMap 的 key 是 ThreadLocal,但它不會傳統的調用 ThreadLocal 的 hashCode 方法(繼承自Object 的 hashCode),而是調用 nextHashCode() ,具體運算如下:
public class ThreadLocal<T> {
//獲取下一個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變量為0x61c88647
ThreadLocal t1 = new ThreadLocal();
//t2中的threadLocalHashCode變量為0x61c88647 + 0x61c88647
ThreadLocal t2 = new ThreadLocal();
//t3中的threadLocalHashCode變量為0x61c88647 + 0x61c88647 + 0x61c88647
ThreadLocal t3 = new ThreadLocal();
threadLocalHashCode是下面的ThreadLocalMap結構中使用的哈希算法的核心變量,對於每個ThreadLocal實例,它的threadLocalHashCode是唯一的。
這里寫個demo看一下基於魔數 1640531527 方式產生的hash分布多均勻:
public class ThreadLocalTest {
public static void main(String[] args) {
printAllSlot(8);
printAllSlot(16);
printAllSlot(32);
}
static void printAllSlot(int len) {
System.out.println("********** len = " + len + " ************");
for (int i = 1; i <= 64; i++) {
ThreadLocal<String> t = new ThreadLocal<>();
int slot = getSlot(t, len);
System.out.print(slot + " ");
if (i % len == 0) {
System.out.println(); // 分組換行
}
}
}
/**
* 獲取槽位
*
* @param t ThreadLocal
* @param len 模擬map的table的length
* @throws Exception
*/
static int getSlot(ThreadLocal<?> t, int len) {
int hash = getHashCode(t);
return hash & (len - 1);
}
/**
* 反射獲取 threadLocalHashCode 字段,因為其為private的
*/
static int getHashCode(ThreadLocal<?> t) {
Field field;
try {
field = t.getClass().getDeclaredField("threadLocalHashCode");
field.setAccessible(true);
return (int) field.get(t);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}
上述代碼模擬了 ThreadLocal 做為 key 的hashCode產生,看看完美槽位分配:
********** len = 8 ************
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
2 1 0 7 6 5 4 3
********** len = 16 ************
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3
********** len = 32 ************
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 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 7 14 21 28 3
Process finished with exit code 0
2. ThreadLocal 之 set() 方法
ThreadLocal中set()方法的源碼如下:
protected T initialValue() {
return null;
}
/**
* 將此線程局部變量的當前線程副本設置為指定值。大多數子類將不需要
* 重寫此方法,而僅依靠{@link #initialValue}
* 方法來設置線程局部變量的值。
*
* @param value 要存儲在此線程的thread-local副本中的值
*/
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,如果原來的哈希槽已經存在值,則進行覆蓋。
3.ThreadLocal 之 get() 方法
ThreadLocal中get()方法的源碼如下:
/**
* 返回此線程局部變量的當前線程副本中的值。如果該變量沒有當前線程的值,
* 則首先通過調用{@link #initialValue}方法將其初始化為*返回的值。
*
* @return 當前線程局部變量中的值
*/
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()方法進行覆蓋。
4.TreadLocal的remove方法
ThreadLocal中remove()方法的源碼如下:
public void remove() {
//獲取Thread實例中的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//根據當前ThreadLocal作為Key對ThreadLocalMap的元素進行移除
m.remove(this);
}
這里羅列了 ThreadLocal 的幾個public方法,其實所有工作最終都落到了 ThreadLocalMap 的頭上,ThreadLocal 僅僅是從當前線程取到 ThreadLocalMap 而已,具體執行,請看下面對 ThreadLocalMap 的分析。
5.內部類ThreadLocalMap的基本結構和源碼分析
ThreadLocalMap 是ThreadLocal 內部的一個Map實現,然而它並沒有實現任何集合的接口規范,因為它僅供內部使用,數據結構采用 數組 + 開方地址法,Entry 繼承 WeakReference,是基於 ThreadLocal 這種特殊場景實現的 Map,它的實現方式很值得研究。
ThreadLocal內部類ThreadLocalMap使用了默認修飾符,也就是包(包私有)可訪問的。ThreadLocalMap內部定義了一個靜態類Entry。我們重點看下ThreadLocalMap的源碼,
5.1先看成員和結構部分
/**
* ThreadLocalMap是一個定制的散列映射,僅適用於維護線程本地變量。
* 它的所有方法都是定義在ThreadLocal類之內。
* 它是包私有的,所以在Thread類中可以定義ThreadLocalMap作為變量。
* 為了處理非常大(指的是值)和長時間的用途,哈希表的Key使用了弱引用(WeakReferences)。
* 引用的隊列(弱引用)不再被使用的時候,對應的過期的條目就能通過主動刪除移出哈希表。
*/
static class ThreadLocalMap {
//注意這里的Entry的Key為WeakReference<ThreadLocal<?>>
static class Entry extends WeakReference<ThreadLocal<?>> {
//這個是真正的存放的值
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 的 key 是 ThreadLocal,但它不會傳統的調用 ThreadLocal 的 hashCode 方法(繼承自Object 的 hashCode),而是調用 nextHashCode()
5.2接着看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<Object> key = (ThreadLocal<Object>) 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()使用的。
5.3ThreadLocalMap 之 set() 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 用key的hashCode計算槽位
// hash沖突時,使用開放地址法
// 因為獨特和hash算法,導致hash沖突很少,一般不會走進這個for循環
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // key 相同,則覆蓋value
e.value = value;
return;
}
if (k == null) { // key = null,說明 key 已經被回收了,進入替換方法
replaceStaleEntry(key, value, i);
return;
}
}
// 新增 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些過期的值,並判斷是否需要擴容
rehash(); // 擴容
}
這個 set 方法涵蓋了很多關鍵點:
- 開放地址法:與我們常用的Map不同,java里大部分Map都是用鏈表發解決hash沖突的,而 ThreadLocalMap 采用的是開發地址法。
- hash算法:hash值算法的精妙之處上面已經講了,均勻的 hash 算法使其可以很好的配合開方地址法使用;
- 過期值清理
下面對 set 方法里面的幾個關鍵方法展開:
1.replaceStaleEntry()
因為開發地址發的使用,導致 replaceStaleEntry 這個方法有些復雜,它的清理工作會涉及到slot前后的非null的slot。
//這里個方法比較長,作用是替換哈希碼為staleSlot的哈希槽中Entry的值
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 往前尋找過期的slot
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 找到 key 或者 直到 遇到null 的slot 才終止循環
// 遍歷staleSlot之后的哈希槽,如果Key匹配則用輸入值替換
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果找到了key,那么需要將它與過期的 slot 交換來維護哈希表的順序。
// 然后可以將新過期的 slot 或其上面遇到的任何其他過期的 slot
// 給 expungeStaleEntry 以清除或 rehash 這個 run 中的所有其他entries。
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果存在,則開始清除前面過期的entry
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果我們沒有在向前掃描中找到過期的條目,
// 那么在掃描 key 時看到的第一個過期 entry 是仍然存在於 run 中的條目。
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果沒有找到 key,那么在 slot 中創建新entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果還有其他過期的entries存在 run 中,則清除他們
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
上文中的 run 不好翻譯,理解為開放地址中一個slot中前后不為null的連續entry
2.cleanSomeSlots()
cleanSomeSlots 清除一些slot(一些?是不是有點模糊,到底是哪些?)
//清理第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); // n = n / 2, 對數控制循環
return removed;
}
當新元素被添加時,或者另一個過期元素已被刪除時,會調用cleanSomeSlots。該方法會試探性地掃描一些 entry 尋找過期的條目。它執行 對數 數量的掃描,是一種 基於不掃描(快速但保留垃圾)和 所有元素掃描之間的平衡。
上面說到的對數數量是多少?循環次數 = log2(N) (log以2為底N的對數),此處N是map的size,如:
log2(4) = 2
log2(5) = 2
log2(18) = 4
因此,此方法並沒有真正的清除,只是找到了要清除的位置,而真正的清除在 expungeStaleEntry(int staleSlot) 里面
3.expungeStaleEntry(int staleSlot)
這里是真正的清除,並且不要被方法名迷惑,不僅僅會清除當前過期的slot,還回往后查找直到遇到null的slot為止。開放地址法的清除也較難理解,清除當前slot后還有往后進行rehash。
//對當前哈希表中所有的Key為null的Entry調用expungeStaleEntry
// 1.清空staleSlot對應哈希槽的Key和Value
// 2.對staleSlot到下一個空的哈希槽之間的所有可能沖突的哈希表部分槽進行重哈希,置空Key為null的槽
// 3.注意返回值是staleSlot之后的下一個空的哈希槽的哈希碼
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清空staleSlot對應哈希槽的Key和Value
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash 直到 null 的 slot
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {//空key直接清除
e.value = null;
tab[i] = null;
size--;
} else {//key非空,則Rehash
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
5.4ThreadLocalMap 之 getEntry() 方法
getEntry() 主要是在 ThreadLocal 的 get() 方法里被調用
/**
* 這個方法主要給`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)//無hash沖突情況
return e;
else // 注意這里,如果e為null或者Key對不上,表示:有hash沖突情況,會調用getEntryAfterMiss
return getEntryAfterMiss(key, i, e);
}
// 如果Key在哈希表中找不到哈希槽的時候會調用此方法
// 這個方法是在遇到 hash 沖突時往后繼續查找,並且會清除查找路上遇到的過期slot。
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;
}
5.5ThreadLocalMap 之 rehash() 方法
// 重哈希,必要時進行擴容
private void rehash() {
// 清理所有空的哈希槽,並且進行重哈希
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// 在上面的清除過程中,size會減小,在此處重新計算是否需要擴容
// 並沒有直接使用threshold,而是用較低的threshold (約 threshold 的 3/4)提前觸發resize
if (size >= threshold - threshold / 4)
resize();
}
// 對當前哈希表中所有的Key為null的Entry調用expungeStaleEntry
private 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);
}
}
// 擴容,簡單的擴大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;
}
PS :ThreadLocalMap 沒有 影響因子 的字段,是采用直接設置 threshold 的方式,threshold = len * 2 / 3,相當於不可修改的影響因子為 2/3,比 HashMap 的默認 0.75 要低。這也是減少hash沖突的方式。
5.6ThreadLocalMap 之 remove(key) 方法
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
remove 方法是刪除特定的 ThreadLocal,建議在 ThreadLocal 使用完后一定要執行此方法。
五、什么情況下ThreadLocal的使用會導致內存泄漏
其實ThreadLocal本身不存放任何的數據,而ThreadLocal中的數據實際上是存放在線程實例中,從實際來看是線程內存泄漏,底層來看是Thread對象中的成員變量threadLocals持有大量的K-V結構,並且線程一直處於活躍狀態導致變量threadLocals無法釋放被回收。threadLocals持有大量的K-V結構這一點的前提是要存在大量的ThreadLocal實例的定義,一般來說,一個應用不可能定義大量的ThreadLocal,所以一般的泄漏源是線程一直處於活躍狀態導致變量threadLocals無法釋放被回收。但是我們知道,·ThreadLocalMap·中的Entry結構的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·),當沒有強引用來引用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<ThreadLocal<?>>。之前一篇文章介紹過,如果弱引用關聯的對象如果置為null,那么該弱引用會在下一次GC時候回收弱引用關聯的對象。舉個例子:
public class ThreadLocalMain {
private static ThreadLocal<Integer> 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代碼塊中調用,這樣能完全避免操作不當導致的內存泄漏,這種主動清理的方式比惰性刪除有效。
七、黃金分割 - 魔數0x61c88647
1.黃金分割數與斐波那契數列
首先復習一下斐波那契數列,下面的推導過程來自某搜索引擎的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就被稱為黃金分割數。證明過程如下:
黃金分割數的准確值為(根號5 - 1)/2,約等於0.618。
2.黃金分割數的應用
黃金分割數被廣泛使用在美術、攝影等藝術領域,因為它具有嚴格的比例性、藝術性、和諧性,蘊藏着豐富的美學價值,能夠激發人的美感。當然,這些不是本文研究的方向,我們先嘗試求出無符號整型和帶符號整型的黃金分割數的具體值:
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);
}
通過一個線段圖理解一下:
也就是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的冪次方的哈希表。