Java ThreadLocal源碼解析 有圖有真相

前言

今天來發第一個源碼相關的博客。先發個最簡單的ThreadLocal試試水吧... 大佬輕噴

經常遇到的問題

ThreadLocal瞭解嗎

弱引用說一下

ThreadLocal會造成內存泄漏嗎

...

之前刷面經經常看到這些問題,所以自己看了一下JDK1.8的ThreadLocal的實現。最近正好複習,把之前沒寫的博客補上

正文

看源碼很容易一頭霧水,覺得不知道我在說什麼的往下翻,有一張processOn做的圖,看完就懂了,所以我們先自己構思一下如果讓我們自己實現這樣一個東西我們要怎麼實現,然後看一下類的結構 包括繼承關係等,最後帶着問題去看就會容易一些。

.......省略思考過程

我們使用ThreadLocal,最常用的api就是set... get...

所以現在我們從set入手,看看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);
        else
            createMap(t, value);
    }

  可以看到這裏有一個getMap(t),然後我們的value被放到了這個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;
    }

清楚地看到返回了t.threadLocals,那麼這個threadLocals是個什麼小餅乾。

public class Thread implements Runnable {

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
}

這是個null。回到ThreadLocal的set(T value)方法。接下來要執行的一定是createmap(t,value)。見名知意,這裏一定是給threadLocals賦值。

所以我們看看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
     */
    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);
        }

好的我們看到了一個Entry,這個table被賦值成了一個Entry數組, 默認長度爲16,並且要求必須是2的冪次

然後給table[i]上創建了一個Entry節點,我們的ThreadLocal引用作爲Key傳了進去,而我們的value作爲v傳了進去。

我們繼續跟蹤Entry。這裏是Entry類的全部代碼

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

哇! 金色傳說!好了 到此set方法就完事了。可以看到這裏Entry是一個弱引用,指向ThreadLocal,並且它還有一個強引用指向我們存進去的那個值。

來畫個圖看看現在的關係是怎樣的

這個圖到底對不對呢,本人水平有限。但我得到的信息就是這樣。所以暫且認爲它沒問題

思考一下爲什麼這裏是一個弱引用

這樣一看就很明顯了。如果我們的強引用threadLocal不再指向ThreadLocal對象,那麼Entity中的弱引用將不會影響垃圾回收器回收ThreadLocal對象。

所以現在你知道ThreadLocal到底會不會造成內存泄漏了嗎

答案是當然會,因爲如果你不手動釋放掉Entity指向“我們set進去”的對象的話,這個對象是不會被GC的。

現在讓我們來看看remove方法做了什麼,是不是跟我們想的一樣,釋放掉我們的對象呢(醒醒 你沒有對象)

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

嗯,它調用了上圖中threadLocals的remove方法,並將當前ThreadLocal的引用傳了進去。所以我們還得繼續跟進去看看發生了什麼。但從這個代碼來看,我們猜的應該是正確的。

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

上面這個代碼就是ThreadLocalMap中的代碼了。可以看到它用set中同樣的方法計算了hash值,然後找到我們的ThreadLocal指向的那個槽位(slot)。然後調用了expungeStaleEntry方法(expunge v. 刪除; stale adj. 過期的)即刪除過期的Entry,並將下標傳了過去

那麼我們再看看expungeStaleEntry幹了什麼

 /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

好了通過這裏我們看到了,它確實讓我們的entry不再指向我們創建的對象,於是我們創建的對象成爲了沒有引用指向的垃圾。下一次GC就會帶走它了。同時可以看到它不止清除了這個slot,還順便清除了下一個null之前的slot,並且做了rehash。勤勞!

最後想說的話

對沒錯就這麼突然的結束了,用大腿想一想也知道get方法做了什麼吧!第一次寫源碼分析的博客... 如果有錯誤請各位大佬指正... 覺得說得還可以的話就點個贊吧! 大家好,我是裝... yes!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章