文章已收錄Github精選,歡迎Star: https://github.com/yehongzhi/learningSummary
什麼是ThreadLocal
ThreadLocal提供線程的局部變量,這種變量與普通變量的差別在于,每個通路這種變量的線程都有自己的、獨立的變量副本。用于解決多線程間的資料隔離問題。
使用場景
其實ThreadLocal在很多開源架構中都有應用:
- Spring中的事務管理器,比如TransactionSynchronizationManager等。
- Mybatis中的ErrorContext類,使用ThreadLocal實作線程安全的單例。
- 存儲session中的一些參數,比如使用者資訊等。
API
ThreadLocal提供了4個常用方法:
-
方法,設定目前線程中變量的副本。set()
-
方法,擷取get()
在目前線程中儲存的變量副本。ThreadLocal
-
方法,清空目前線程中變量的副本。remove()
-
是一個initialValue()
方法,一般是用來重寫的,如果在沒有set的時候就調用protected
,會調用get
方法初始化内容。initialValue
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
//重寫此方法,初始化ThreadLocal的value
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
原理
那麼怎麼實作資料隔離的,我們從源碼的角度進行分析。
我們先看ThreadLocal類的get()方法。
public T get() {
Thread t = Thread.currentThread();
//通過目前線程擷取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
//傳回Thread執行個體的成員變量threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//給Thread執行個體的成員變量threadLocals指派
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
從源碼可以看出,資料是存在于Thread類的成員變量threadLocals
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
上面寫了一段注釋,翻譯過來就是,關于該線程的ThreadLocal的值,由ThreadLocal類進行維護。
是以很清楚了,資料隔離的實作是因為ThreadLocal類操作的是Thread的成員變量threadLocals。每個線程Thread都有自己的threadLocals,進而互相不影響。
threadLocals這個成員變量的本質又是ThreadLocalMap類,它是ThreadLocal的内部類,下面我們研究一下這個内部類的資料結構。
資料結構
先看一下源碼:
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;
}
}
//初始化容量
private static final int INITIAL_CAPACITY = 16;
//散清單
private Entry[] table;
//有效數量
private int size = 0;
//負載因子
private int threshold;
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//構造器
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還有幾分相似,但是哈希沖突的處理方式,ThreadLocalMap采用的是開放尋址法(自行百度一下,這裡不多解釋了),大概長這個樣子:

是以這裡可以看出ThreadLocal的引用可以定位到ThreadLocalMap裡散清單table[]裡的值。
記憶體洩漏問題
我們從源碼中可以看到Entry是繼承WeakReference類,key是弱引用,value是強引用。為什麼要設計成弱引用?不如反過來想,如果設定成強引用會有什麼效果。
如果Entry對象的Key每個都強引用到ThreadLocal對象的話,那麼這個ThreadLocal對象就會因為和Entry對象存在強引用關聯而無法被GC回收,造成記憶體洩漏,除非線程結束後,線程被回收了,ThreadLocalMap才會跟着回收。
當作為Key的ThreadLocal對象設定成弱引用對象後,在系統GC的時候,ThreadLocal對象就會被回收。
但是這樣就能防止記憶體洩漏嗎?
其實不然!因為Value還是強引用對象,當Key被回收後,key變成了null值,而Value依然存在一條強引用鍊:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,而這塊value也永遠不會被通路到了,最終造成記憶體洩漏。
是以在設計ThreadLocalMap時就考慮到這個問題,在ThreadLocal的
get()、set()、remove()
的時候都會清除線程ThreadLocalMap裡所有key為
null
的value。
總結
- 其實ThreadLocal并沒有解決多線程間資料共享的問題,而是使資料在不同線程有不同的副本,那麼就不需要解決共享資料的問題。
- 每個線程持有一個ThreadLocalMap對象,該ThreadLocalMap對象隻會被持有它的線程通路,是以不存線上程安全問題。
- ThreadLocalMap的資料結構類似HashMap,裡面由Entry[]數組、size、負載因子等組成,采用開放尋址法解決哈希沖突。
- ThreadLocalMap的Entry對ThreadLocal對象是弱引用,GC回收後,會産生一些key為null的value無法被通路,也無法被回收,最終導緻記憶體洩漏。預防措施是調用ThreadLocal的remove()方法,清除掉ThreadLocalMap裡面key為null的value。
非常感謝你的閱讀,希望這篇文章能給到你幫助和啟發。
覺得有用就點個贊吧,你的點贊是我創作的最大動力~
我是一個努力讓大家記住的程式員。我們下期再見!!!
能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!