講透ThreadLocal
一. 簡介
ThreadLocal是JDK提供的一個工具類,其作用是在多線程共享資源的情況下,使每個線程持有一份該資源的副本,每個線程的副本都是獨立互不影響的。線程操作各自的副本,這樣就避免了資源競爭引發的線程安全問題。
二. 使用示例
模擬Spring中的事務管理,每個事務與當前線程綁定,不同線程的事務之間相互獨立互不影響。代碼如下:
public class TransactionManager {
//業務線程
private static final class BizTask implements Runnable{
private final ThreadLocal<Transaction> transaction;
public BizTask(){
//創建事務,並與當前線程綁定
transaction = ThreadLocal.withInitial(() -> {
long id = Thread.currentThread().getId();
return new Transaction(id);
});
}
@Override
public void run() {
transaction.get().begin();
System.err.println("執行業務邏輯");
try {
Thread.sleep(1000L);
transaction.get().commit();
transaction.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//事務
private static final class Transaction {
private long id;
private TransactionStatus status;
public Transaction(long id) {
this.id = id;
}
public void begin(){
status = TransactionStatus.BEGIN;
System.err.println("開啓事務, id: " + id + ", status: " + status);
}
public void commit() {
status = TransactionStatus.COMMIT;
System.err.println("提交事務, id: " + id + ", status: " + status);
}
public void rollback() {
status = TransactionStatus.ROLLBACK;
System.err.println("回滾事務, id: " + id + ", status: " + status);
}
}
//事務狀態
private enum TransactionStatus {
BEGIN,
COMMIT,
ROLLBACK,
}
public static void main(String[] args) throws InterruptedException {
int threadNum = 5;
ExecutorService executor = Executors.newFixedThreadPool(threadNum);
//執行業務邏輯,可以看到每個線程的事務相互獨立,互不影響
for (int i = 0; i < threadNum; i++) {
executor.submit(new BizTask());
}
executor.shutdown();
}
}
結果如下:
開啓事務, id: 17, status: BEGIN
執行業務邏輯
開啓事務, id: 14, status: BEGIN
執行業務邏輯
開啓事務, id: 13, status: BEGIN
執行業務邏輯
開啓事務, id: 16, status: BEGIN
執行業務邏輯
開啓事務, id: 15, status: BEGIN
執行業務邏輯
提交事務, id: 14, status: COMMIT
提交事務, id: 17, status: COMMIT
提交事務, id: 13, status: COMMIT
提交事務, id: 16, status: COMMIT
提交事務, id: 15, status: COMMIT
三. ThreadLocal源碼分析
ThreadLocal,線程本地變量,該變量爲每個線程私有。ThreadLocal類有一個內部類,名爲ThreadLocalMap,可以理解爲一個簡化版的HashMap。源碼如下:
static class ThreadLocalMap {
//該Map的Entry,Key爲ThreadLocal實例,Value爲ThreadLocal對象所引用的值。
//這裏使用了WeakReference弱引用,當Entry爲null時可以儘快被GC
static class Entry extends WeakReference<ThreadLocal<?>> {
//與ThreadLocal關聯的對象引用,爲當前Entry的value
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始容量16
private static final int INITIAL_CAPACITY = 16;
//使用數組保存了所有的線程本地變量
private Entry[] table;
}
其中Entry爲ThreadLocalMap的一個內部類,與HashMap的Entry結構類似,都是key-value對的形式。它的Key爲ThreadLocal實例,Value爲ThreadLocal對象所引用的對象。ThreadLocalMap使用一個Entry[]數組保存了所有的線程本地變量,因爲一個線程可以維護多個ThreadLocal實例。
ThreadLocalMap內部保存了衆多的ThreadLocal對象,既然說ThreadLocal是線程私有的,那麼ThreadLocalMap是存放在哪裏呢?
Thread類有一個成員變量——threadLocals,它就是保存了與當前Thread關聯的一個ThreadLocalMap,源碼如下:
//當前線程內部維護的ThreadLocalMap對象,用於保存所有ThreadLocal實例
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到,ThreadLocalMap對象保存在了Thread的內部,也即當前線程的私有內存中。通過上面的分析,我們可以瞭解ThreadLocal變量的大致內存結構如下:
ThreadLocal的主要方法爲get()、set()和initialValue()。首先看set():
public void set(T value) {
//獲取當前線程
Thread t = Thread.currentThread();
//獲取當前線程關聯的ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
//創建一個Entry,以當前ThreadLocal對象爲Key,待存儲對象爲Value,保存在ThreadLocalMap中
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到,set()的邏輯很簡單,從當前線程中獲取ThreadLocalMap,然後將該ThreadLocal的值保存在裏面。
再看get()方法:
public T get() {
//獲取當前線程對象
Thread t = Thread.currentThread();
//獲取當前線程關聯的ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
//從ThreadLocalMap獲取以ThreadLocal爲key的Entry的value
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果當前ThreadLocalMap不存在,則調用setInitialValue()方法,獲取初始值
return setInitialValue();
}
ThreadLocal還有一個方法initialValue(),該方法提供給子類覆蓋,以在創建ThreadLocal時指定初始值。
四. 應用場景
ThreadLocal最常見的使用場景爲管理數據庫連接Connection對象等。Spring中使用ThreadLocal來設計TransactionSynchronizationManager類,實現了事務管理與數據訪問服務的解耦,同時也保證了多線程環境下connection的線程安全問題。
五. ThreadLocal的內存泄漏問題
-
JVM引用類型
從源碼中可以看出,ThreadLocalMap中Entry的key爲ThreadLocal對象,並將其聲明成了一個WeakReference弱引用。在分析其設計思想之前,先簡單回顧下JVM中的幾種引用類型:
- 強應用:普通的引用類型,被強引用的對象是一定不會被GC回收的。
- 軟引用SoftReference:軟引用一般用來實現內存敏感的緩存,如果有空閒內存就可以保留緩存,當內存不足時就清理掉,這樣就保證使用緩存的同時不會耗盡內存。
- 弱引用WeakReference:它的生命週期比軟引用還要短,在GC的時候,不管內存空間是否夠用,都會回收WeakReference對象。
- 虛引用PhantomReference(較少使用):任何時候可能被GC回收,就像沒有引用一樣。
ThreadLocal之所以設計成WeakReference,目的就是當外部不再持有ThreadLocal的強引用時,儘快回收該ThreadLocalMap中對應的key。
-
ThreadLocal的內存泄漏問題
如前文所述,當ThreadLocal沒有外部強引用來引用它時,ThreadLocal對象就會在下次JVM垃圾收集時被回收,這個時候就會出現Entry中Key已經被回收,但是Value仍然所在,即所謂的"null Key"情況。此時外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前線程的生命週期很長,那麼其內部的ThreadLocalMap對象也一直生存下來,這些null key就會一直存在一條強引用鏈:Thread --> ThreadLocalMap–>Entry–>Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收了,進而造成內存泄漏。
-
JDK對於ThreadLocal內存泄漏的解決方案
JDK團隊已經考慮到這樣的情況,並做了一些措施來使得ThreadLocal儘量不會出現內存泄漏:在ThreadLocal的get()、set()、remove()方法調用的時候,會清除掉ThreadLocalMap的所有Entry中Key爲null的Value,並將整個Entry設置爲null,利於下次內存回收。
以ThreadLocal的get()方法爲例:
public T get() { //獲取當前線程實例 Thread t = Thread.currentThread(); //獲取當前線程中的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { //通過ThreadLocalMap的getEntry()方法獲取Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
它會調用ThreadLocalMap的getEntry()方法獲取Entry實例:
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else //如果Entry獲取不到,則調用getEntryAfterMiss() return getEntryAfterMiss(key, i, e); }
如果獲取不到,說明該key不存在或已經被回收了,則進入getEntryAfterMiss()方法:
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; //如果key爲null,則清除該Entry項 if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
注意這裏的k == null的情況,如果key爲null,則執行expungeStaleEntry清除該Entry項。
此外,JDK推薦當ThreadLocal對象不再使用時,顯式調用其remove()方法,清除該線程本地變量,最終也會調用上面的ThreadLocalMap.getEntryAfterMiss()方法。
綜上,JDK也提供了相應的策略儘量避免ThreadLocal導致的內存泄漏問題,主要是在get()和remove()操作時清除已被回收的Entry項。但是該策略也並不是完美的,如果用戶將ThreadLocal初始化後,再也不調用get()或remove()方法,則還是有內存泄漏的風險。
-
爲什麼要使用WeakReference?
從前文的描述中,很可能造成一種錯覺:ThreadLocal由於使用了WeakReference而導致了內存泄漏。這其實是沒有真正理解ThreadLocal內存泄漏的本質。首先我們來看看爲什麼要使用弱引用。下面是官方文檔的說法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. Entry的key使用WeakReference弱引用,來處理大內存、長生命週期的線程的使用問題。
我們分別考慮下不使用WeakReferences和使用WeakReferences的情況下,分別會造成什麼問題:
- 不使用WeakReferences,而使用強引用:外部引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致整個Entry內存泄漏。
- 使用使用WeakReferences:引用的ThreadLocal的對象被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set()、get()或remove()的時候會被清除。如果沒有調用,則可能造成Entry的Value的內存泄漏。
由此可以看出,無論是否使用WeakReference,都有可能產生內存泄漏的情況,其根本原因在於ThreadLocalMap的生命週期與線程綁定。如果線程存活時間較長,且沒有顯式remove掉ThreadLocal對象,就有可能出現問題。而使用了WeakReference,至少可以保證無用的ThreadLocal對象被回收,不會出現整個Entry的內存泄漏,在一定程度上緩解了該問題。
-
總結
ThreadLocal的內存泄漏問題,根本原因是ThreadLocalMap的生命週期與Thread綁定,如果線程執行時間較長,則ThreadLocalMap就會一直不被GC回收。如果不顯式調用remove()方法移除過期的ThreadLocal,則有可能造成內存泄漏。因此建議使用ThreadLocal時線程生命週期不要過長,且ThreadLocal對象使用完後顯式調用remove()方法進行移除。