ThreadLocal也叫「線程本地變量」、「線程局部變量」:html
ThreadLocal表明了一種線程與任務剝離的思想,從而達到線程封閉
的目的,幫助咱們設計出更「健康」(簡單,美觀,易維護)的線程安全類。java
ThreadLocal的使用方法每每給咱們形成一種假象——變量封裝在任務對象內部。git
咱們在使用ThreadLocal對象的時候,每每是在任務對象內部聲明ThreadLocal變量,如:github
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的實現中最重要的一點就是——「將線程相關的域封閉在當前線程實例中」,雖然域仍然在任務對象中聲明、set和get,卻與任務對象無關。ide
下面從源碼中分析ThreadLocal的實現。函數
首先觀察看ThreadLocal類的源碼,找到構造函數:源碼分析
/** * Creates a thread local variable. * @see #withInitial(java.util.function.Supplier) */ public ThreadLocal() { }
空的。ui
直接看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引用。
如今,咱們已經很好的「將線程相關的域封閉在當前線程實例中」,若是線程死亡,線程中的ThreadLocalMap實例也將被回收。
看起來一切都那麼美好,但咱們忽略了一個很嚴重的問題——若是任務對象結束而線程實例仍然存在(常見於線程池的使用中,須要複用線程實例),那麼仍然會發生內存泄露。咱們能夠建議廣大Javaer手動remove聲明過的ThreadLocal變量,但這種迴歸C++的語法是不能被Javaer接受的;另外,要求我猿記住聲明過的變量,簡直比約妹子吃飯還困難。
對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
出處:https://monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。