原始碼|ThreadLocal的實現原理

monkeysayhi發表於2019-03-04

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引用。

  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
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章