源碼|ThreadLocal的實現原理

ThreadLocal也叫「線程本地變量」、「線程局部變量」:html

  • 其做用域覆蓋線程,而不是某個具體任務;
  • 其「天然」的生命週期與線程的生命週期「相同」(但在JDK實現中比線程的生命週期更短,減小了內存泄漏的可能)。

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對象仍然存在。咱們不能要求線程在死亡以前主動刪除其使用的ThreadLocal對象,因此valueMap中該線程對應的entry(<key, 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引用。

  1. ThreadLocal說明了Entry的類型,保存的是ThreadLocal變量。
  2. 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
出處:https://monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。

相關文章
相關標籤/搜索