1. 程式人生 > >ThreadLocal原始碼分析解密

ThreadLocal原始碼分析解密

什麼是ThreadLocal

我們來看看作者Doug Lea是怎麼說的,下面是jdk7.x裡面ThreadLocal註釋

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g.a user ID or Transaction ID).
each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist)

也就是說這個類給執行緒提供了一個本地變數,這個變數是該執行緒自己擁有的。在該執行緒存活和ThreadLocal例項能訪問的時候,儲存了對這個變數副本的引用.當執行緒消失的時候,所有的本地例項都會被GC。並且建議我們ThreadLocal最好是 private static 修飾的成員

和Thread的關係

假設我們要設計一個和執行緒繫結的變數,我們會怎麼做呢?很常見的一個思路就是把Thread和變數放在一個Map

 /**
     * 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 ThreadLocalMap map = getMap(t); //map存在,則把value放入該ThreadLocalMap中 if (map != null) map.set(this, value); else createMap(t, value); }

然後,看看getMap方法做了些什麼

 /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        //返回Thread的一個成員變數
        return t.threadLocals;
    }

原來是把ThreadLocalMap和Thread繫結起來了,Thread類中有一個ThreadLocalMap為null的變數,那我們現在回到ThreadLocalMap來看,在我們Thread返回的引用來看,如果map為null的情況下,呼叫了createMap方法.這就為我們的Thread建立了一個能儲存在本地執行緒的map.下面是Thread裡面的欄位

    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    //ThreadLocal幫助Thread賦值了該欄位
    ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap

那麼當我們第一次使用ThreadLocal的時候,我們通過getMAP得到的ThreadLocalMap必然是null,我們來看看createMap方法

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

        /**
         * Construct a new map initially containing(firstKey,firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        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);
        }

在CreatMap中會直接new 一個ThreadLocalMap,裡面傳入的是當前ThreadLocal#this.然後建立一個大小為INITIAL_CAPACITY的Entry。關於這個INITIAL_CAPACITY為什麼是2的N次方,這在HashMap裡面也是有體現的,這裡INITIAL_CAPACITY為16那麼16-1=15在二進位制中就是1111.當他和TheadLocal的INITIAL_CAPACITY相與的時候,得到的數絕對是<=INITIAL_CAPACITY.這和threadLocalHashCode%INITIAL_CAPACITY的效果是一樣的,但是效率比前者好處很多倍。ok,這裡不再贅述,此時我們已經得到一個下標位置,我們直接new了一個Entry(ThreadLocal,Object),放入該table陣列當中,這個時候把table的size置為1,閾值職位INITIAL_CAPACITY的2/3(達到最大長度的2/3的時候會擴容).程式碼就不貼了。

這裡小結一下,現在我們應該能夠理清ThreadLocal和Thread的關係了,大致是這樣的:Thread裡面有一個類似MAP的東西,但是初始化的時候為null,當我們使用ThreadLocal的時候,ThreadLocal會幫助當前執行緒初始化這個MAP,並且把我們需要和執行緒繫結的值放入改Map中。map的key為當前ThreadLocal。那麼這樣和我們才開始的想法有什麼不一樣呢,才開始我們的想法是在ThreadLocal當中維護一個mao,key為Thread表示,value為值。和這樣的方式有什麼差別呢,為什麼要這樣做?話說在jdk1.3之前就是用這種方式做的,但是之後就改成了現在的這種做法。這樣做法的優點之一是,value放在了執行緒當中,隨著執行緒的生命週期生存,執行緒死亡,value回收。之二是效能提高了,想想一下在有很多請求的應用中,如果按照之前的做法,HashMap該多大?,效能應該會比較低,而換成後者這種方法,map的大小變得比較小,和Threadlocal的數量相同(有多少個ThreadLocal,執行緒當中的map實際儲存的就有多少個)。

可能存在的問題

上文看似我們已經漸漸的明白了ThreadLocal的本質,實際上Threadlocal可能會存在一些些問題
關於Entry,這裡說一下,jdk中Entry的key值其實是弱引用的,這代表他將會很快被GC掉

 /**
         * 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);
                value = v;
            }
        }

如下圖(摘自網路),ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用引用他,那麼系統gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前執行緒再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈,這就會造成很多人都認為的記憶體洩露,其實我認為是不會發生的。繼續看
在這裡插入圖片描述

按照道理來說,如果我們使用的執行緒池方式,當一個執行緒使用完的時候,執行緒並沒有死亡,而是迴歸線程池繼續使用,這個時候和該執行緒的bind其實並有沒什麼意義呢,但是呢?value並不會被回收,這也算導致了記憶體洩露,還有一種情況就是上述所說的,當弱引用被回收吊,null無法訪問value,也導致了相同的問題。那麼?這是真的嗎?先賣一個關子,我們先來看看一些其他的東西

ThreadLocal小片段

ThreadLocal之間是如何區分的呢?給每個ThreadLocal一個識別符號?這確實是一種思路,jdk裡面是這樣做的。

//每個物件都有一個HashCode來標示自己的唯一性
 private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
     //原子類保證執行緒安全,保證每個物件的hashcode唯一,並且是靜態的
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     為什麼是這個數,暫時沒探究
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     返回原始值,加上上面那個數
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

比如我第一個ThreadLocal的hashCode就是0,那麼我在定義一個他的hashCode就是0的基礎上加上HASH_INCREMENT。這樣在map中他們的hahscode不一樣,但是這個時候雖然hashcode不一樣,但是計算出來的下標i可能是一樣的,這就造成了hash衝突,在ThreadLocal裡面用的解決Hash衝突是用的線性探查法(Linear Probing)來解決的,當i下標有值的時候則找到i+1處,然後依次往下推。看看set、get

 /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //map不為null.直接在map中取
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        //map為null,需要從初始化的地方取值
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        //初始化值的方法,大部分情況我們會重寫這個方法
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
        //放入map
            map.set(this, value);
        else
        //新建map
            createMap(t, value);
        return value;
    }

    /**
     * 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);
        else
            createMap(t, value);
    }

粗虐上來看,這是一個非常簡單的對map的add、get、init操作,但是我們來看看ThreadLocalMap#set方法的一些細節

 private void set(ThreadLocal key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.
            //得到應該與運算之後應該得到的下標
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            /*得到entry,如果e不為null,呼叫父類Reference的get方法得到 ThreadLocal物件,雖然下標相同。但是很可能不是同一個ThreadLocal物件,
如果是同一個物件,k==key。就替換Entry裡面的value值,該下標的物件k為null。就放入改位置,如果有其他的,就往下一個i+1位置上找            
            */
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            /*如果計算後的座標獲取到的entry為null,就new一個Entry物件並儲存進去,然後呼叫cleanSomeSlots()對table進行清理,如果沒有任何Entry被清理,並且表的size超過了閾值,就會呼叫rehash()方法。 
cleanSomeSlots()會呼叫expungeStaleEntry清理陳舊過時的Entry。rehash則會呼叫expungeStaleEntries()方法清理所有的陳舊的Entry,然後在size大於閾值的3/4時呼叫resize()方法進行擴容。程式碼如下*/
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

在get中getEntry()方法通過計算出的下標從table中取出entry,如果取得的entry為null或它的key值不相等,就呼叫getEntryAfterMiss()方法,否則返回。
而在getEntryAfterMiss()是當通過key與table的長度取模得到的下標取得entry後,entry裡沒有該key時所呼叫的。這時,如果獲取的entry為null,即沒有儲存,就直接返回null,否則進入迴圈不,計算下一個座標並獲取對應的entry,並且當key相等時(表明找到了之前儲存的值)返回entry,或是entry為null時退出迴圈,並返回null。expungeStaleEntries方法會清楚所有key未null的Entry

  /**
         * Expunge all stale entries in the table.
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }

總結

到了最後,上面我們留下的問題大致也都得到了答案,在我們呼叫set或者get的時候,ThreadLocal會自動的清楚key為null的值,不會造成記憶體洩露。而當使用執行緒池的時候,我們應該在改執行緒使用完該ThreadLocal的時候自覺地呼叫remove方法清空Entry,這會是一個非常好的習慣。
被廢棄了的ThreadLocal所繫結物件的引用,會在以下4情況被清理。

  1. List item
  2. Thread結束時。
  3. 當Thread的ThreadLocalMap的threshold超過最大值時。rehash
  4. 向Thread的ThreadLocalMap中存放一個ThreadLocal,hash演算法沒有命中既有Entry,而需要新建一個Entry時。
  5. 手工通過ThreadLocal的remove()方法或set(null)。