ThreadLocal也叫“執行緒本地變數”、“執行緒區域性變數”:
- 其作用域覆蓋執行緒,而不是某個具體任務;
- 其“自然”的生命週期與執行緒的生命週期“相同”(但在JDK實現中比執行緒的生命週期更短,減少了記憶體洩漏的可能)。
ThreadLocal代表了一種執行緒與任務剝離的思想,從而達到執行緒封閉
的目的,幫助我們設計出更“健康”(簡單,美觀,易維護)的執行緒安全類。
一種假象
ThreadLocal的使用方法往往給我們造成一種假象——變數封裝在任務物件內部。
根據使用方法推測實現思路
我們在使用ThreadLocal物件的時候,往往是在任務物件內部宣告ThreadLocal變數,如:
ThreedLocal<List> onlineUserList = …;複製程式碼
這也是此處說“從概念上”可以視作如此的原因。那麼從這種使用方法的角度出發(逆向思維),讓我們自然而然的認為,ThreadLocal變數是儲存在任務物件內部的,那麼實現思路如下:
class ThreadLocal<T>{
private Map<Thread, T> valueMap = …;
public T get(Object key){
T realValue = valueMap.get(Thread.currentThread())
return realValue;
}
}複製程式碼
存在問題
但是這樣實現存在一個問題:
- 執行緒死亡之後,任務物件可能仍然存在(這才是最普遍的情況),從而ThreadLocal物件仍然存在。我們不能要求執行緒在死亡之前主動刪除其使用的ThreadLocal物件,所以valueMap中該執行緒對應的entry()無法回收;
問題的本質在於,這種實現“將執行緒相關的域封閉於任務物件,而不是執行緒中”。所以ThreadLocal的實現中最重要的一點就是——“將執行緒相關的域封閉在當前執行緒例項中”,雖然域仍然在任務物件中宣告、set和get,卻與任務物件無關。
原始碼分析
下面從原始碼中分析ThreadLocal的實現。
逆向追蹤原始碼
首先觀察看ThreadLocal類的原始碼,找到建構函式:
/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}複製程式碼
空的。
直接看set方法:
/**
* Sets the current thread`s copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread`s copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); // 使用this引用作為key,既做到了變數相關,又滿足key不可變的要求。
else
createMap(t, value);
}複製程式碼
設定value時,要根據當前執行緒t獲取一個ThreadLocalMap型別的map,真正的value儲存在這個map中。這驗證了之前的一部分想法——ThreadLocal變數儲存在一個“執行緒相關”的map
中。
進入getMap方法:
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}複製程式碼
可以看到,該map實際上儲存在一個Thread例項中,也就是之前傳入的當前執行緒t。
觀察Thread類的原始碼,確實存在著threadLocals變數的宣告:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;複製程式碼
在這種實現中,ThreadLocal變數已經達到了文章開頭的提出的基本要求:
- 其作用域覆蓋執行緒,而不是某個具體任務
- 其“自然”的生命週期與執行緒的生命週期“相同”
如果是面試的話,一般分析到這裡就可以結束了。
進階
希望進一步深入,可以繼續檢視ThreadLocal.ThreadLocalMap類的原始碼:
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 使用了WeakReference中的key
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;複製程式碼
可以看到,雖然ThreadLocalMap沒有實現Map介面,但也具有常見Map實現類的大部分屬性(與HashMap不同,hash重複時在table裡順序遍歷)。
重要的是Entry的實現。Entry繼承了一個ThreadLocal泛型的WeakReference引用。
- ThreadLocal說明了Entry的型別,儲存的是ThreadLocal變數。
- WeakReference是為了方便垃圾回收。
WeakReference與記憶體洩露
仍然存在的記憶體洩露
現在,我們已經很好的“將執行緒相關的域封閉在當前執行緒例項中”,如果執行緒死亡,執行緒中的ThreadLocalMap例項也將被回收。
看起來一切都那麼美好,但我們忽略了一個很嚴重的問題——如果任務物件結束而執行緒例項仍然存在(常見於執行緒池的使用中,需要複用執行緒例項),那麼仍然會發生記憶體洩露。我們可以建議廣大Javaer手動remove宣告過的ThreadLocal變數,但這種迴歸C++的語法是不能被Javaer接受的;另外,要求我猿記住宣告過的變數,簡直比約妹子吃飯還困難。
使用WeakReference減少記憶體洩露
對ThreadLocal原始碼的分析讓我第一次瞭解到WeakReference的作用的使用方法。
對於弱引用WeakReference,當一個物件僅僅被弱引用指向, 而沒有任何其他強引用StrongReference指向的時候, 如果GC執行, 那麼這個物件就會被回收。
在ThreadLocal變數的使用過程中,由於只有任務物件擁有ThreadLocal變數的強引用(考慮最簡單的情況),所以任務物件被回收後,就沒有強引用指向ThreadLocal變數,ThreadLocal變數也就會被回收。
之所以這裡說“減少記憶體洩露”,是因為單純使用WeakReference僅僅解決了問題的前半部分。
進一步減少記憶體洩露
儘管現在使用了弱引用,ThreadLocalMap中仍然會發生記憶體洩漏。原因很簡單,ThreadLocal變數只是Entry中的key,所以Entry中的key雖然被回收了,Entry本身卻仍然被引用。
為了解決這後半部分問題,ThreadLocalMap在它的getEntry、set、remove、rehash等方法中都會主動清除ThreadLocalMap中key為null的Entry。
這樣做已經可以大大減少記憶體洩露的可能,但如果我們宣告ThreadLocal變數後,再也沒有呼叫過上述方法,依然會發生記憶體洩露。不過,現實世界中執行緒池的容量總是有限的,所以這部分洩露的記憶體並不會無限增長;另一方面,一個大量執行緒長期空閒的執行緒池(這時記憶體洩露情況可能比較嚴重),也自然沒有存在的必要,而一旦使用了執行緒,洩露的記憶體就能夠被回收。因此,我們通常認為這種記憶體洩露是可以“忍受”的。
同時應該注意到,這裡將ThreadLocal物件“自然”的生命週期“收緊”了一些,從而比執行緒的生命週期更短。
其他記憶體洩露情況
還有一種較通用的記憶體洩露情況:使用static關鍵字宣告變數。
使用static關鍵字延長了變數的生命週期,可能導致記憶體洩露。對於ThreadLocal變數也是如此。
更多記憶體洩露的分析可參見ThreadLocal 記憶體洩露的例項分析,這裡不再展開。
參考:
本文連結:原始碼|ThreadLocal的實現原理
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。