重學Java——ThreadLocal源碼解讀

ThreadLocal

今天端午,就看一下輕鬆點的東西吧,上次說消息機制,說到Looper時,就是把Looper存儲在ThreadLocal中,而後在對應的線程獲取到對象,今天就來看下ThreadLocal的源碼解讀吧。html

ThreadLocal的簡單使用

仍是上次講的那個例子java

final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    threadLocal.set(1);
    new Thread(new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "run: "+threadLocal.get());     
        }
    }).start();
複製代碼

獲得的結果是***MainActivity: run: null,可見在哪一個線程放在數據,只有在對應的那個線程取出。程序員

ThreadLocal源碼分析

每個線程Thread的源碼內部有屬性面試

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

再看ThreadLocal的源碼,主要關心的就是存儲問題,也就是set和get方法,先來看下set算法

public void set(T value) {
        //獲取當前線程
        Thread t = Thread.currentThread();
        //獲得ThreadLocalMap,這是個專門用於存儲線程的ThreadLocal的數據
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
複製代碼

看第二行ThreadLocalMap map = getMap(t);,這是個專門用於存儲線程的ThreadLocal的數據,set的步驟是:編程

  1. 獲取當前線程的成員變量map
  2. map非空,則從新將ThreadLocal和新的value副本放入到map中。
  3. map空,則對線程的成員變量ThreadLocalMap進行初始化建立,並將ThreadLocal和value副本放入map中。
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        //每個線程中都一個對應的threadLocal,而後又經過ThreadLocal負責來維護對應的ThreadLocalMap
        //經過ThreadLocal來獲取來設置線程的變量值
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
複製代碼

先暫時不去看ThreadLocalMap的源碼,只要知道它是用於存儲就行,咱們先看下get方法數組

public T get() {
        Thread t = Thread.currentThread();
        //仍是能過當前線程get到這個threadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                //從map裏取到值就直接返回
                return result;
            }
        }
        //沒有取到值就返回默認初始值
        return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
        //當前線程的ThreadLocal
        return t.threadLocals;
    }

	
    private T setInitialValue() {
        //這個等下看,看字面意思就是初始化value
        T value = initialValue();
        Thread t = Thread.currentThread();
        //下面的就是和set方法就是同樣的了
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
複製代碼

再看一下initialValue方法併發

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

就返回了一個null,爲何不直接用null呢,這也是複製這一大段註釋的緣由,此實現只返回{@code null};若是程序員但願線程局部變量的初始值不是{@code null},則必須對{@code ThreadLocal}進行子類化,而且此方法將被重寫。一般,將使用匿名內部類。less

再回到get方法,能夠得出get的步驟爲:ide

  1. 獲取當前線程的ThreadLocalMap對象threadLocal
  2. 從map中獲取線程存儲的K-V Entry節點。
  3. 從Entry節點獲取存儲的Value副本值返回。
  4. map爲空的話返回初始值null,即線程變量副本爲null,在使用時須要注意判斷NullPointerException。

ThreadLocalMap

如今咱們集中精力來看ThreadLocalMap的源碼

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

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    
        private static final int INITIAL_CAPACITY = 16;
    
        private Entry[] table;

        private int size = 0;

        private int threshold;
}
複製代碼

上面是ThreadLocalMap的一些屬性,結構看起來和HashMap結構差很少,能夠看到ThreadLocalMap的Entry繼承自WeakReference,並使用ThreadLocal爲鍵值。

這裏爲何不使用普通的key-value形式來定義存儲結構,實質上就會形成節點的生命週期與線程綁定,只要線程沒有銷燬,那麼節點在GC是一直是處於可達狀態,是沒辦法回收的,而程序自己並無方法判斷是否能夠清理節點。弱引用的性質就是GC到達時,那麼這個對象就會被回收。當某個ThreadLocal已經沒有強引用可達,則隨着它被GC回收,在ThreadLocalMap裏對應的Entry就會失效,這也爲Map自己垃圾清理提供了便利。

/** * Set the resize threshold to maintain at worst a 2/3 load factor. * 設置resize閾值以維持最壞2/3的負載因子 */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /** * Increment i modulo len. * 下一個索引 */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /** * Decrement i modulo len. * 上一個索引 */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }
複製代碼

熟悉HashMap的話,其實對負載因子應該很熟悉,ThreadLocal有兩個方法用於獲得上/下一個索引,用於解決Hash衝突的方式就是簡單的步長加1或減1,尋找下一個相鄰的位置。

因此很明顯,ThreadLocalMap這種線性探測方式來解決Hash衝突效率很低,建議:每一個線程只存一個變量,這樣的話全部的線程存放到map中的key都是相同的ThreadLocal,若是一個線程要保存多個變量,就須要建立多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增長Hash衝突的可能。

再來看它的set方法

private void set(ThreadLocal<?> key, Object value) {
            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();
		
                //找到對應的entry
                if (k == key) {
                    e.value = value;
                    return;
                }

                //替換失效的entry
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            //若是沒有找到對應的key,就在末尾放上new Entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                //再次hash
                rehash();
        }


        private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            //向前探測
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 //固定步長
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            //向後遍歷
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                //找到key,更新爲新的value
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //若是在整個掃描過程當中,找到了以前的無效值,那麼以它爲清理起點,不然以當前的i爲清理起點
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            //若是key在table中不存在,在原地放一個new entry
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            //在探測過程當中發現無效的位置,則作一次清理(連續段清理+啓發式清理)
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

       private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    //清理一個連續段
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

        private void rehash() {
            //清理陳舊數據
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            // 清理完後,若是>=3/4閾值,就進行擴容
            if (size >= threshold - threshold / 4)
                resize();
        }
複製代碼
  • 在set方法中,先循環查找,若是key的值找到了,直接替換覆蓋就可
  • 若是k失效,那麼直接調用replaceStaleEntry,效果就是把這個key和value都放在這個位置,同時病理歷史key=null的陳舊數據
  • 若是沒有找到key,那麼在末尾後的一個空位置放上entry,放完後作一次清理,若是清理出去key,而且當前是table大小已經超過閾值,則作一次rehash。
    • 清理一遍陳舊數據
    • >= 3/4閥值,就執行擴容,把table擴容2倍==》注意這裏3/4閥值就執行擴容,避免遲滯
    • 把老數據從新哈希散列進新table

能夠看到,和HashMap最大的不一樣在於,ThreadLocalMap的結構很是簡單,沒有next引用,就是說ThreadLocalMap解決Hash衝突並非鏈表的方式,而是線性探測——當key的hashcode值在table數組的位置,若是發現這個位置上已經有其餘的key值元素佔用了,那麼利用固定的算法尋找下必定步長的下一個位置,依次判斷,直到找到可以存放的位置

ThreadLocal的問題

對於ThreadLocal的內存泄漏,因爲ThreadLocalMap的key是弱引用,而value是強引用,這就致使當ThreadLocal在沒有外部對象強引用時,GC會回收key,但value不會回收,若是ThreadLocal的線程一直運行着,那麼這個Entry對象的value就可能一直不能回收,引起內存泄漏。

ThreadLocal的設計已經爲咱們考慮到了這個問題,提供remove方法,將Entry節點和Map的引用關係移除,這樣Entry在GC分析時就變成了不可達,下次GC就能回收。

看一下remove的源碼

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
複製代碼

能夠看到就是調用了ThreadLocalMap的remove方法

/** * 從map中刪除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) {
                    //調用entry的clear方法
                    e.clear();
                    //進行清理
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
複製代碼

因此咱們在使用後,能夠顯示的調用remove方法,來避免內存泄漏,是一個很好的編程習慣。

參考

ThreadLocal終極源碼剖析-一篇足矣!

ThreadLocal-面試必問深度解析

Java併發編程:深刻剖析ThreadLocal


個人CSDN

下面是個人公衆號,歡迎你們關注我

相關文章
相關標籤/搜索