ThreadLocal源碼解讀

ThreadLocal<T>介紹

咱們知道在Java併發編程中咱們通常會使用synchronized關鍵字或者CAS操做類來進行共享資源的同步。ThreadLocal類爲併發編程提供了另一種思路,它將共享資源做爲每個線程的副本,這樣在某些場景下,咱們就能夠無需同步完成併發程序的設計與開發,Andoid中Looper類的實現裏面就使用到了ThreadLocal。
ThreadLocal本質是本地線程副本工具類,將線程與線程私有變量作一個映射,各個線程之間的變量互不干擾。 java

ThreadLocal類全覽

ThreadLocal存儲結構

經過結構圖咱們能夠總結出如下幾點:

  • 每個Thread線程內部都有一個threadLocals對象。
  • threadLocals是一個數組結構,每一項存儲的是Entry結構,其中key是ThreadLocal對象,value是線程的變量副本。
  • ThreadLocal做爲一個工具類,提供方法對線程的threadLocals對象進行維護,支持設置、獲取、移除等操做。

ThreadLocal源碼分析

從上面的類全覽圖咱們能夠看到,ThreadLocal類提供的主要方法有set(T)、get()、remove()、initialValue()等,下面咱們就從這些經常使用的方法開始分析。編程

get()方法

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);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
複製代碼

從源碼中咱們看到第一行就是獲取當前線程對象,而後調用getMap獲取ThreadLocalMap對象,咱們繼續往下跟,看getMap是什麼邏輯。bash

/**
     * 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;
    }
複製代碼

咱們看到這個方法直接返回的就是線程對象的threadLocals成員變量,咱們繼續往下跟,在Thread.java文件中有以下定義併發

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

通過上面的分析,咱們能夠梳理出get()方法的相關操做步驟:
less

  • 獲取到當前線程的ThreadLocalMap類型對象threadLocals.
  • 將當前ThreadLocal對象做爲key,從map中獲取相應的Entry(包含key,value結構). 相關代碼以下:
/**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
複製代碼
  • 若是map爲空,即當前線程中沒有存儲變量副本,那麼就調用setInitialValue()方法來設置默認值(默認值能夠經過initialValue來進行自定義,默認返回null)。咱們來看下這個方法的邏輯:
/**
     * 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.set(this, value);
        else
            createMap(t, value);
        return value;
    }
複製代碼

這個方法的邏輯就是:獲取設置的默認值,而後將其設置到map中。ide

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);
    }
複製代碼

set()方法的邏輯以下:
函數

  • 獲取當前線程的ThreadLocalMap對象map.
  • 若是map!=null就將新值更新到map中,不然建立map來存儲第一個設置的值,咱們來看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);
    }
複製代碼

initialValue()方法

該方法是用來設置默認值的,即map中找不到數據時返回的默認值,咱們在初始化ThreadLocal的時候能夠自定義這個默認值,源碼以下:工具

/**
     * Returns the current thread's "initial value" for this * thread-local variable. This method will be invoked the first * time a thread accesses the variable with the {@link #get} * method, unless the thread previously invoked the {@link #set} * method, in which case the {@code initialValue} method will not * be invoked for the thread. Normally, this method is invoked at * most once per thread, but it may be invoked again in case of * subsequent invocations of {@link #remove} followed by {@link #get}. * * <p>This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code ThreadLocal} must be * subclassed, and this method overridden. Typically, an * anonymous inner class will be used. * * @return the initial value for this thread-local */ protected T initialValue() { return null; } 複製代碼

咱們能夠像下面這樣,自定義默認值:oop

ThreadLocal<String> threadLocal=new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "default";
    }
};
複製代碼

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); } 複製代碼

remove()方法比較簡單,就是移除map中的副本。

ThreadLocalMap解析

ThreadLocalMap類圖:

在ThreadLocalMap中,是使用Entry結構來存儲K-V數據的,key是ThreadLocal對象,這個在構造函數中已經指定了,源碼以下:

/**
         * 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; } } 複製代碼

Entry繼承WeakReference(弱引用,生命週期只能存活到下次GC前),但只有Key是弱引用類型的,Value並不是弱引用。

ThreadLocalMap成員變量

static class ThreadLocalMap {
        /**
         * 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;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
}
複製代碼

從源碼作咱們看到,ThreadLocalMap類結構很簡單,主要結構是一個Entry[]數組,來存放每個線程存放的變量副本。Entry結構和HashMap的Node結構仍是有很大不一樣的,Node結構須要存儲next節點,發生衝突後會造成鏈表結構,而Entry結構只是單純的K-V結構。那麼若是發生衝突如何解決?咱們去set方法裏面尋找答案。

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be 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);

            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;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
複製代碼

從set()方法中咱們能夠看到,先根據key的hashCode來肯定元素在table列表中的位置,而後判斷是否存在數據,若是存在數據就更新操做。而後e = tab[i = nextIndex(i, len)])這一句很關鍵,咱們看到會調用nextIndex方法獲取下一個節點。源碼以下:

/**
     * Increment i modulo len.
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
複製代碼

這裏使用的是固定步長來查找下一個能夠存放的位置。如今咱們能夠總結ThreadLocalMap解決Hash衝突時經過固定步長,查找上一個或者下一個位置來存放Entry數據。

固定步長引入的效率問題

若是有大量的ThreadLocal對象存入ThreadLocalMap對象中,會致使衝突,甚至是二次衝突,這就必定程度上下降了效率。那咱們如何解決呢? 良好建議:
每一個線程只存一個變量,這樣全部的線程存放到map中的Key都是相同的ThreadLocal,若是一個線程要保存多個變量,就須要建立多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增長Hash衝突的可能。若是須要存儲多份數據,可使用包裝類進行包裝下。

ThreadLocal內存泄漏探究

不少文章都會提到不正確的使用ThreadLocal會致使內存泄漏的發生,這一節咱們來好好研究下內存泄漏時如何發生的。 咱們來看一下在使用ThreadLocal對象時,對象的內存分佈:

ThreadLocal的原理:每一個Thread內部維護着一個ThreadLocalMap,它是一個Map。這個映射表的Key是一個弱引用,其實就是ThreadLocal自己,Value是真正存的線程變量Object。

也就是說ThreadLocal自己並不真正存儲線程的變量值,它只是一個工具,用來維護Thread內部的Map,幫助存和取。注意上圖的虛線,它表明一個弱引用類型,而弱引用的生命週期只能存活到下次GC前。

ThreadLocal內存泄漏緣由

ThreadLocal對象在ThreadLocalMap對象中是使用一個弱引用進行被Entry中的Key進行引用的,所以若是ThreadLocal對象沒有外部強引用來引用它,那麼ThreadLocal對象會在下次GC的時候被回收(注意:若是ThreadLocal對象還有強引用引用,GC事後,WeakReference仍是不會被回收的)。這時候Entry結構中就會出現Null Key的狀況。外部讀取ThreadLocal是沒法 使用Null Key來找到Value的。所以若是當前線程執行時間足夠長的話,就會造成一條強引用的鏈。Thread-->ThreadLocalMap對象-->Entry(Key爲Null,Value還引用其餘對象)-->Object對象。這就致使了Entry對象不會被回收,固然Object對象也不會回收。

相關解決方案

ThradLocal類的設計者也考慮到這種狀況了,因此在ThreadLocal類的相關操做方法中,例如:get()、set()、remove()等方法中都會尋找Key爲Null的Entry節點,將Entry的Key和Value結構都設置爲Null,利於GC回收。下面咱們就來看相關方法。

/**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
    
    
        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code * @param e the entry at table[i] * @return the entry associated with key, or null if no such'
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
複製代碼

從上面的源碼咱們看到,在get方法中會調用getEntry()方法,getEntry()方法內部會根據entry對象和key是不是Null執行getEntryAfterMiss()方法。咱們在getEntryAfterMiss()方法內部能夠看到k==null時執行的是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刪除後,會繼續循環往下尋找是否存在Key爲Null的節點,若是存在的話也刪除,防止內存泄漏發生。

ThreadLocal爲何使用弱引用

有不少人以爲之因此發生內存泄漏是由於ThreadLocal中Entry結構Key是弱引用致使的。實際上是由於Entry中Key爲Null以後,沒有主動清除Value所致使的。那Entry結構爲何使用弱引用呢?官方的註釋是這樣的:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
爲了處理很是大和運行週期很是長的線程。哈希表使用弱引用。 
複製代碼

下面咱們分兩種狀況來看:

  • ThreadLocal對象使用強引用 引用的ThreadLocal的對象被回收了,可是ThreadLocalMap還持有ThreadLocal的強引用,若是沒有手動刪除,ThreadLocal不會被回收,致使Entry內存泄漏。
  • ThreadLocal對象使用弱引用 引用的ThreadLocal的對象被回收了,因爲ThreadLocalMap持有ThreadLocal的弱引用,即便沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。 比較兩種狀況,咱們能夠發現:因爲ThreadLocalMap的生命週期跟Thread同樣長,若是都沒有手動刪除對應key,都會致使內存泄漏,可是使用弱引用能夠多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。

總結

每次使用完ThreadLocal,都調用它的remove()方法,清除數據。 在使用線程池的狀況下,沒有及時清理ThreadLocal,不只是內存泄漏的問題,更嚴重的是可能致使業務邏輯出現問題。因此,使用ThreadLocal就跟加鎖完要解鎖同樣,用完就清理。

相關文章
相關標籤/搜索