作者 | 蔡柱梁
審校 | 重樓
一、前言
很多 Java 開發一般都是做中臺較多,并發編程使用的不多。因此,對 ThreadLocal 不太熟悉,所以筆者這里想讓大家了解它,知道它是用來干什么的。
二、ThreadLocal 是用來干什么的
ThreadLocal 是 Java 中一種線程封閉技術,它提供了一種線程本地變量的機制,使得每個線程都擁有一個獨立的變量副本,這樣可以避免多個線程訪問同一個變量時產生的并發問題。
ThreadLocal 在工作中還是蠻常用的,筆者使用到的一些場景如下:
- 使用 zk 實現選舉,采用單例 zkClient,但是對于里面一些全局變量就會存在線程安全問題,這時會希望這些特定的全局變量可以跟線程綁定。
- 項目UUC(統一認證中心),不同的用戶登錄,系統是如何確保當前用戶的信息不會被張冠李戴的呢?其實都是通過 ThreadLocal 實現的(不過在 UUC 中,筆者使用的是 InheritableThreadLocal,這個會有點區別)。
- 參數傳遞,比如流水生成的方法里面的重試機制,假設限制重試 5 次,生成流水號的方法內部很多地方都可能失敗需要重試(并發沖突或者 db 異常),最傳統的方式就是將重試的次數傳遞。這種方式不夠優雅,我們可以使用 ThreadLocal 來實現傳遞。
總的來說,當你需要和線程綁定的變量時,就可以考慮使用 ThreadLocal 啦!
至于線程安全問題,大家不妨想想我們平常說線程安全問題都是出現在什么場景?同一時間有兩個或兩個以上的線程對同一個變量進行修改,才有可能出現線程安全問題。但是使用 ThreadLocal,每個線程是獨享自己的變量副本的,哪里還有線程安全問題呢?
三、ThreadLocal 如何使用
這個上網一搜一大堆,筆者就說下注意事項好了,用完后一定要釋放,避免內存泄漏,提供幾個點給大家參考:
- 及時清理
- 確保在線程結束時,及時清理 ThreadLocal 中存儲的數據。可以通過在使用完 ThreadLocal 后調用 remove() 方法來清理對應的數據。例如,可以使用 ThreadLocal.remove() 或在 finally 塊中進行清理操作。
- 使用弱引用(WeakReference)
- 可以使用 ThreadLocal 的變體,如 InheritableThreadLocal 或 WeakThreadLocal,它們使用了弱引用來存儲數據。這樣,在沒有其他強引用指向被存儲的對象時,垃圾回收器可以自動清理該對象,避免內存泄漏。
- 避免長時間存儲大量數據
- 盡量避免在 ThreadLocal 中存儲大量數據,特別是對于長時間運行的線程。因為 ThreadLocal 的值在線程的整個生命周期中都存在,如果存儲大量數據,可能會導致內存占用過高。
- 及時釋放資源
- 如果你在 ThreadLocal 中存儲了需要手動釋放的資源,確保在不再需要時及時釋放資源。可以通過在使用完資源后顯式地調用資源的釋放方法或使用 try-with-resources 語句來實現。
- 防止線程池中的內存泄漏
- 當使用線程池時,要特別小心使用 ThreadLocal。確保在任務完成后清理 ThreadLocal 中的數據,以避免線程重用時的數據干擾和潛在的內存泄漏問題。可以在任務的開始和結束處使用 ThreadLocal 進行數據綁定和解綁。
總之,要正確使用 ThreadLocal 并避免內存泄漏問題,需要注意適時清理、使用弱引用、避免存儲過多數據、及時釋放資源,并在使用線程池時特別小心。
四、ThreadLocal 的實現原理
下面是一個簡單的示例代碼:
public class ThreadLocalExample {
private static final ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread workerThread = new Thread(() -> {
try {
// 在線程中設置ThreadLocal值
threadLocal.set(new Object());
// 執行業務邏輯
// ...
} finally {
// 在線程結束時清理ThreadLocal值
threadLocal.remove();
}
});
workerThread.start();
// 等待線程結束
try {
workerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}在示例代碼中,線程 workerThread 和 ThreadLocal 實例是一個怎樣的關系呢?set 方法和 remove 方法都做了什么呢?為什么會有內存泄漏的情況呢?我們帶著疑問一起往下看。
4.1 java.lang.ThreadLocal#set
我們直接從源碼開始分析 ThreadLocal。
public void set(T value) {
// 獲取當前線程
Thread t = Thread.currentThread();
// 通過當前線程獲取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}結合示例代碼來看,這里是當前線程A在 main 方法中通過 threadLocal 實例調用 threadLocal.set 方法,而 set 方法會給當前線程創建一個 ThreadLocalMap(如果沒有的話),并使用 threadLocal 實例作為 key。
它們的關系如下圖:

4.2 內存泄漏問題
這里應該分成兩種情況看:無線程復用和有線程復用。
- 無線程復用
當 workerThread 結束后,沒有強引用的 ThreadLocalMap 自然而然也會被垃圾回收器回收,不會出現內存泄漏。 - 有線程復用
這里也要分開看,有釋放和無釋放的情況。如果發生內存泄漏,當然就是我們沒有釋放導致的(釋放可以通過調用 set、get、remove方法釋放)。當我們使用線程池,線程會被復用時,ThreadLocalMap 的生命周期與它綁定的線程是一樣的,所以不會被回收。如果這時發生了 gc,那么 Entry 的 key 是弱引用,key 會變成 null,而 value 將繼續存活。如果該線程一直不調用 set/get/remove 方法,那么 value 一直得不到釋放,就會發生內存泄漏的現象。
那為什么使用 set/get/remove 可以避免內存泄漏呢?因為 set/get 在根據當前線程找到對應 Entry 元素后(這里是剛好是碰到了 key==null 的 entry[i],碰不到是不會順手釋放舊 value 的。因此,最好還是使用完后調用 remove 釋放),發現 key == null,就會調用java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry 釋放引用,所以就不會發生內存泄漏了。這里就不再展示源碼了,有興趣的可以自己去看下。
五、哈希沖突問題
上面看到 ThreadLocalMap 使用了 Hash,是不是馬上就想到了哈希沖突呢?HashMap 遇到哈希沖突,在 key 不相同的情況下,會使用鏈表解決。但是 ThreadLocalMap 的 Entry 沒有 next 指針,因此它明顯不會采用鏈表,那么它是如何解決哈希沖突的呢?
請看 java.lang.ThreadLocal.ThreadLocalMap#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;
// 存在哈希沖突的話,會往下走,如果超過數組長度,就會回到0
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 找到存儲自己的entry,更新value
e.value = value;
return;
}
if (k == null) {
// 因為 gc 導致 key 被回收了,這個 Entry 會被新的 Entry 取代(新的Entry的key和value就是這里的傳參),舊的會被釋放
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}總結
到這里相信大家對 ThreadLocal 都有了一定的了解。有什么想交流可以留言或私信筆者。
作者介紹
蔡柱梁,51CTO社區編輯,從事Java后端開發8年,做過傳統項目廣電BOSS系統,后投身互聯網電商,負責過訂單,TMS,中間件等。






























