有關 ThreadLocal 的一切
- 2022 年 5 月 10 日
- 筆記
早上好,各位新老讀者們,我是七淅(xī)。
今天和大家分享的是面試常駐嘉賓:ThreadLocal
當初鵝廠一面就有問到它,問題的答案在下面正文的第 2 點。
1. 底層結構
ThreadLocal 底層有一個默認容量為 16 的數組組成,k 是 ThreadLocal 對象的引用,v 是要放到 TheadLocal 的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
數組類似為 HashMap,對哈希衝突的處理不是用鏈表/紅黑樹處理,而是使用鏈地址法,即嘗試順序放到哈希衝突下標的下一個下標位置。
該數組也可以進行擴容。
2. 工作原理
一個 ThreadLocal 對象維護一個 ThreadLocalMap 內部類對象,ThreadLocalMap 對象才是存儲鍵值的地方。
更準確的說,是 ThreadLocalMap 的 Entry 內部類是存儲鍵值的地方
見源碼 set()
,createMap()
可知。
因為一個 Thread 對象維護了一個 ThreadLocal.ThreadLocalMap 成員變量,且 ThreadLocal 設置值時,獲取的 ThreadLocalMap 正是當前線程對象的 ThreadLocalMap。
// 獲取 ThreadLocalMap 源碼
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
所以每個線程對 ThreadLocal 的操作互不干擾,即 ThreadLocal 能實現線程隔離
3. 使用
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在學Java");
Integer i = threadLocal.get()
// i = 七淅在學Java
4. 為什麼 ThreadLocal.ThreadLocalMap 底層是長度 16 的數組呢?
對 ThreadLocal 的操作見第 3 點,可以看到 ThreadLocal 每次 set 方法都是對同個 key(因為是同個 ThreadLocal 對象,所以 key 肯定都是一樣的)進行操作。
如此操作,看似對 ThreadLocal 的操作永遠只會存 1 個值,那用長度為 1 的數組它不香嗎?為什麼還要用 16 長度呢?
好了,其實這裡有個需要注意的地方,ThreadLocal 是可以存多個值的
那怎麼存多個值呢?看如下代碼:
// 在主線程執行以下代碼:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在學Java");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
threadLocal2.set("七淅在學Java2");
按代碼執行後,看着是 new 了 2 個 ThreadLocal 對象,但實際上,數據的存儲都是在同一個 ThreadLocal.ThreadLocalMap 上操作的
再次強調:ThreadLocal.ThreadLocalMap 才是數據存取的地方,ThreadLocal 只是 api 調用入口)。真相在 ThreadLocal 類源碼的 getMap()
因此上述代碼最終結果就是一個 ThreadLocalMap 存了 2 個不同 ThreadLocal 對象作為 key,對應 value 為 七淅在學Java、七淅在學Java2。
我們再看下 ThreadLocal 的 set
方法
public void set(T value) {
Thread t = Thread.currentThread();
// 這裡每次 set 之前,都會調用 getMap(t) 方法,t 是當前調用 set 方法的線程
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 重點:返回調用 set 方法的線程(例子是主線程)的 ThreadLocal 對象。
// 所以不管 api 調用方 new 多少個 ThreadLocal 對象,它永遠都是返回調用線程(例子是主線程)的 ThreadLocal.ThreadLocalMap 對象供調用線程去存取數據。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// t.threadLocals 的聲明如下
ThreadLocal.ThreadLocalMap threadLocals = null;
// 僅有一個構造方法
public ThreadLocal() {
}
5. 數據存放在數組中,那如何解決 hash 衝突問題
使用鏈地址法解決。
具體怎麼解決呢?看看執行 get、set 方法的時候:
- set:
- 根據 ThreadLocal 對象的 hash 值,定位到 ThreadLocalMap 數組中的位置。
- 如果位置無元素則直接放到該位置
- 如果有元素
- 且數組的 key 等於該 ThreadLocal,則覆蓋該位置元素
- 否則就找下一個空位置,直到找到空或者 key 相等為止。
- get:
- 根據 ThreadLocal 對象的 hash 值,定位到 ThreadLocalMap 數組中的位置。
- 如果不一致,就判斷下一個位置
- 否則則直接取出
// 數組元素結構
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
6. ThreadLocal 的內存泄露隱患
三個前置知識:
- ThreadLocal 對象維護一個 ThreadLocalMap 內部類
- ThreadLocalMap 對象又維護一個 Entry 內部類,並且該類繼承弱引用
WeakReference<ThreadLocal<?>>
,用來存放作為 key 的 ThreadLocal 對象(可見最下方的 Entry 構造方法源碼),可見最後的源碼部分。 - 不管當前內存空間足夠與否,GC 時 JVM 會回收弱引用的內存
因為 ThreadLocal 作為弱引用被 Entry 中的 Key 變量引用,所以如果 ThreadLocal 沒有外部強引用來引用它,那麼 ThreadLocal 會在下次 JVM 垃圾收集時被回收。
這個時候 Entry 中的 key 已經被回收,但 value 因為是強引用,所以不會被垃圾收集器回收。這樣 ThreadLocal 的線程如果一直持續運行,value 就一直得不到回收,導致發生內存泄露。
如果想要避免內存泄漏,可以使用 ThreadLocal 對象的 remove() 方法
7. 為什麼 ThreadLocalMap 的 key 是弱引用
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;
}
}
}
為什麼要這樣設計,這樣分為兩種情況來討論:
- key 使用強引用:只有創建 ThreadLocal 的線程還在運行,那麼 ThreadLocalMap 的鍵值就都會內存泄漏,因為 ThreadLocalMap 的生命周期同創建它的 Thread 對象。
- key 使用弱引用:是一種挽救措施,起碼弱引用的值可以被及時 GC,減輕內存泄漏。另外,即使沒有手動刪除,作為鍵的 ThreadLocal 也會被回收。因為 ThreadLocalMap 調用 set、get、remove 時,都會先判斷之前該 value 對應的 key 是否和當前調用的 key 相等。如果不相等,說明之前的 key 已經被回收了,此時 value 也會被回收。因此 key 使用弱引用是最優的解決方案。
8. (父子線程)如何共享 ThreadLocal 數據
- 主線程創建 InheritableThreadLocal 對象時,會為 t.inheritableThreadLocals 變量創建 ThreadLocalMap,使其初始化。其中 t 是當前線程,即主線程
- 創建子線程時,在 Thread 的構造方法,會檢查其父線程的 inheritableThreadLocals 是否為 null。從第 1 步可知不為 null,接着 將父線程的 inheritableThreadLocals 變量值複製給這個子線程。
- InheritableThreadLocal 重寫了 getMap, createMap, 使用的都是 Thread.inheritableThreadLocals 變量
如下:
public class InheritableThreadLocal<T> extends ThreadLocal<T>
關鍵源碼:
第 1 步:對 InheritableThreadLocal 初始化
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
第 2 步:創建子線程時,判斷父線程的 inheritableThreadLocals 是否為空。非空進行複製
// Thread 構造方法中,一定會執行下面邏輯
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
第 3 步:使用對象為第 1 步創建的 inheritableThreadLocals 對象
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
}
示例:
// 結果:能夠輸出「父線程-七淅在學Java」
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("父線程-七淅在學Java");
Thread t = new Thread(() -> System.out.println(threadLocal.get()));
t.start();
// 結果:null,不能夠輸出「子線程-七淅在學Java」
ThreadLocal threadLocal2 = new InheritableThreadLocal();
Thread t2 = new Thread(() -> {
threadLocal2.set("子線程-七淅在學Java");
});
t2.start();
System.out.println(threadLocal2.get());
文章首發公眾號:七淅在學Java ,持續原創輸出 Java 後端乾貨。
如果對你有幫助的話,可以給個贊再走嗎