ThreadLocal源碼閱讀

  • 趣鏈的面試中被問到ThreadLocal的相關問題,被問的一臉懵*,因此有次總結.

ThreadLocal

  • 線程局部變量是我一直對他的叫法,剛開始接觸是用來保存jdbc的鏈接(這樣想一想我接觸的還挺早的)
  • 做用是爲每一個線程保存線程私有的變量.以空間換時間,也能保證數據的安全性.
  • ThreadLocal並非底層的集合類,而是一個工具類,全部的線程私有數據都被保存在各個Thread對象中一個叫作threadLocalsThreadLocalMap的成員變量裏,ThreadLocal也只是操做這些變量的工具類.
  • 也就是說每一個Thread都會存有一個ThreadLocalMap的對象供多個ThreadLocal的類調用,因此你能夠發現多個ThreadLocal操做的Map會是同一個,而當ThreadLocal做爲key的發生哈希碰撞時,會從當前位置開始向後環型遍歷,找到一個空位置,這方法咱們能夠稱之爲線性探測法.

ThreadLocalMap

  • ThreadLocalMap出人意料的並無繼承任何一個類或接口,是徹底獨立的類。
成員變量
// 默認的初始容量 必定要是二的次冪
        private static final int INITIAL_CAPACITY = 16;
        // 元素數組/條目數組
        private Entry[] table;
       	// 大小,用於記錄數組中實際存在的Entry數目
        private int size = 0;
		// 閾值
        private int threshold; // Default to 0 構造方法
複製代碼
構造方法
// 默認訪問權限的初始化方法
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 使用默認的`容量`初始化數組
            table = new Entry[INITIAL_CAPACITY];
            // 以`ThreadLocal`的`HashCode`計算下標
            // 這裏和HashMap中的計算方式同樣,都用與運算
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 賦值 修改大小並計算閾值
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // `setThreshold`方法也特別簡單,就是2/3的容量。
            setThreshold(INITIAL_CAPACITY);
        }
複製代碼
元素獲取相關方法
getEntry
  • ThreadLocalKey獲取對應的Entryjava

  • 由於ThreadLocalMap底層也是使用數組做爲數據結構,因此該方法也借鑑了HashMap中求元素下標的方式.面試

  • 在獲取的元素爲空的時候還會調用getEntryAfterMiss作後續處理.數組

private Entry getEntry(ThreadLocal<?> key) {
         	// 和HashMap中同樣的下標計算方式
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
    		// 獲取到對應的Entry以後就分兩步
            if (e != null && e.get() == key)
                // 1. e不爲空且threadLocal相等
                return e;		
            else														
                // 2. e爲空或者threadLocal不相等 
                return getEntryAfterMiss(key, i, e);
        }
複製代碼
getEntryAfterMiss
  • 該方法是在直接按照Hash計算下標後,沒獲取到對應的Entry對象的時候調用。
  • 經過遍歷整個數組的方式獲取相同key表示的Entry對象。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
			// 此時注意若是從上面狀況`2.`進來時,
       		// e爲空則直接返回null,不會進入while循環
       		// 只有e不爲空且e.get() != key時纔會進while循環
            while (e != null) {
                ThreadLocal<?> k = e.get();
                // 找到相同的k,返回獲得的Entry,get操做結束
                if (k == key)
                    return e;
                // 若此時的k爲空,那麼e則被標記爲`Stale`須要被`expunge`
                if (k == null)
                    expungeStaleEntry(i);
                else	// 下面兩個都是遍歷的相關操做
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
複製代碼
expungeStaleEntry
  • 該方法用來清除staleSlot位置的Entry對象,而且會清理當前節點到下一個null節點中間的過時Enyru.
  • 是消除內存泄漏威脅的主力方法,在整個ThreadLocalMap中會屢次調用.
/** * 清空舊的Entry對象 * @param staleSlot: 清理的起始位置 * @param return: 返回的是第一個爲空的Entry下標 */
    private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
        	// 清空`staleSlot`位置的Entry
        	// value引用置爲空以後,對象被標記爲不可達,下次GC就會被回收.
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            Entry e;
            int i;
        	// 經過nextIndex從`staleSlot`的下一個開始向後遍歷Entry數組,直到e不爲空
         	// e賦值爲當前的Entry對象
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;		
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                // 當k爲空的時候清空節點信息
                if (k == null) {							
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {	// 如下爲k存在的狀況
                    int h = k.threadLocalHashCode & (len - 1);
                    // 元素下標和key計算的不同,代表是出現`Hash碰撞`以後調整的位置
                    // 將當前的元素移動到下一個null位置
                    if (h != i) {					
                        tab[i] = null;
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        } 
複製代碼
Set相關方法
set
  • 由於ThreadLocalMap底層結構和HashMap同樣也是數組,也是經過hash肯定下標,也同樣會發生Hash碰撞,咱們知道在HashMap中爲了解決Hash碰撞的問題選擇了拉鍊法,但對於ThreadLocalMap並無那麼高的複雜度,因此此處選擇的是開放地址法.
  • 從下方源碼也能夠看出來,Entry再肯定數組位置以後直接就開始了遍歷,若是key不匹配就日後遍歷找到key匹配的元素覆蓋,或者key == null的替換.
private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
    		// 整個循環的功能就是找到相同的key覆蓋value
    		// 或者找到key爲null的節點覆蓋節點信息
    		// 只有在e==null的時候跳出循環執行下面的代碼
            for (Entry e = tab[i];
                 e != null;	
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
				// 找到相等的k,則直接替換value,set操做結束
                if (k == key) {
                    e.value = value;
                    return;
                }
				// k爲空表示該節點過時,直接替換該節點
                if (k == null) {					       // 1.
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			// 走到這一步就是找到了e爲空的位置,否則在上面兩個判斷裏都return了
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
複製代碼
replaceStaleEntry
  • 源碼中只有從上面1.處進入該方法,用於替換key爲空的Entry節點,順帶清除數組中的過時節點.
/** * 從`set.1.`處進入,key是插入元素ThreadLocal的hash,staleSlot爲key爲空的數組節點下標 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            int slotToExpunge = staleSlot;
    		// 從傳入位置,即插入時發現k爲null的位置開始,向前遍歷,直到數組元素爲空
    		// 找到最前面一個key爲null的值. 
    		// 這裏要吐槽一下源代碼...大括號都不加 習慣真差
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;		
                 i = prevIndex(i, len)){
                if (e.get() == null)
                    // 由於是環狀遍歷因此此時slotToExpunge是可能等於staleSlot的
                    slotToExpunge = i;
            }
   			// 該段循環的功能就是向後遍歷找到`key`相等的節點並替換
    		// 並對以後的元素進行清理
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == key) {	
                    // e就是tab[i],因此下三行代碼的功能就是替換Entry
                    // 新的Entry實際仍是在staleSlot下標的位置
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    // 由於接下來要進行清理操做,因此此處須要從新肯定清理的起點.
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
                // 其實我對這個`slotToExpunge == staleSlot`的判斷一直挺疑惑的,爲何須要這個判斷?
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }
    		// e==null時跳到下面代碼運行
    		// 清空並從新賦值
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);
			// set後的清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

複製代碼
cleanSomeSlots
  • 該方法的功能是就是清除數組中的過時Entry
  • 首次清除從i向後開始遍歷log2(n)次,若是之間發現過時Entry會直接將n擴充到len能夠說全數組範圍的遍歷.發現過時Entry就調用expungeStaleEntry清除直到未發現Entry爲止.
/** * @param i 清除的起始節點位置 * @param n 遍歷控制,每次掃描都是log2(n)次,通常取當前數組的`size`或`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) {
                    // 當發現有過時`Entry`時,n變爲len
                    // 即擴大範圍,全數組範圍在遍歷一次
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }	
                // 無符號右移一位至關於n = n /2
                // 因此在第一次會遍歷`log2(n)`次
            } while ( (n >>>= 1) != 0);
    		// 遍歷過程當中沒出現過時`Entry`的狀況下會返回是否有清理的標記.
            return removed;
        }
複製代碼
擴容調整方法
rehash
  • 容量調整的先驅方法,先清理過時Entry,並作是否須要resize的判斷
  • 調整的條件是當前size大於閾值的3/4就進行擴容
private void rehash() {
     		// 清理過時Entry
            expungeStaleEntries();
     		// 初始閾值threshold爲10
            if (size >= threshold - threshold / 4)
                resize();
        }
複製代碼
resize
  • 擴容的實際方法.
private void resize() {
      		// 獲取舊數組並記錄就數組大小
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
      		// 新數組大小爲舊數組的兩倍
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;
			// 遍歷整個舊數組,並遷移元素到新數組
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                // 判斷是否爲空,空的話就算了
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    // k爲空即表示爲過時節點,立即清理了.
                    if (k == null) {
                        e.value = null; 
                    } else {
                        // 從新計算數組下標,若是數組對應位置已存在元素
                        // 則環狀遍歷整個數組找個空位置賦值
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
			// 設置新屬性 
            setThreshold(newLen);
            size = count;
            table = newTab;
        }
複製代碼

  • ThreadLocal的內部方法由於邏輯都不復雜,不須要單獨出來看,就直接全放一塊了.

Get方法

  • 整個獲取的過程其實並不難,因此我說ThreadLocal的精華只要仍是在TheradLocalMap和這種空間換時間的結構.
    1. 首先經過getMap方法獲取當前線程綁定的threadLocals
    2. 不要爲空時,以當前ThreadLocal對象爲參數獲取對應的Entry對象.爲空跳到第四步
    3. 獲取Entry對象中的value,並返回
    4. 調用setInitialValue方法,
// 直接獲取線程私有的數據
   public T get() {
        // 獲取當前線程
        Thread t = Thread.currentThread();
        // getMap其實很簡單就是獲取`t`中的`threadLocals`,代碼在`工具方法`中
        ThreadLocalMap map = getMap(t); 
        if (map != null) {										// 3.
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {							// 2.
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();  				// 1.
    }
	// 這個方法只有在上面`1.`處調用...不知道爲何map,thread不直接傳參
	// 該方法的功能就是爲`Thread`設置`threadLocals`的初始值
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // map不爲null代表是從上面的`2.`處進入該方法
        // 已經初始化`threadLocals`,但並未找到當前對應的`Entry`
        // 因此此時直接添加`Entry`就行
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
      // 初始值,`protected`方便子類繼承,並定義本身的初始值.
      protected T initialValue() {
        return null;
      }

	// 建立並賦值`threadLocals`的方法
     void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
複製代碼

Set方法

  1. 獲取當前線程,並以此獲取線程綁定的ThreadLocalMap對象.
  2. map不爲空時,直接set就好
  3. map爲空時須要先建立並賦值.
public void set(T value) {
        // 獲取當前線程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);     // .1
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
 }
複製代碼
工具方法
getMap(Thraed t)
  • 獲取t中保留的ThreadLocalMap類型的對象
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
複製代碼

ThreadLocal相關問題

ThreadLocal的內存泄漏問題
內存泄漏的緣由
  • 首先對於對做爲keyThreadLocal對象,由於是弱引用咱們徹底不用擔憂,強引用斷開以後天然會被GC回收.安全

  • 再來看value,按照上面所說的做爲成員變量存儲在每一個Thread實例的threadLocals纔是存儲數據的對象,那麼它的生命週期是和Thread相同的,即便將ThreadLocalGC回收, 但對應的value對象仍然存在thread -> threadLocals -> value引用 -> value對象的引用關係,因此GC會認爲它可達,並不會作回收處理,但在咱們現有的代碼中並無可以跳過key去獲取value的,也就是說實際上value已經不可達了.這樣就形成了內存泄漏.數據結構

內存泄漏的處理方法
  • 究其根本仍是斷開value的引用關係,就是講value引用置null.
  • 能夠看到ThreadLcoalMap的方法多處調用了expungeStaleEntry,cleanSomeSlots檢查數組中的Entry對象是否過時,也就是key是否爲空.
ThreadLocal的併發性問題
  • 首先併發問題在我理解中就是多線程狀況下對共享資源的合理使用,像是ReentrantLock,Synchronized都是幫咱們解決共享資源的使用問題.
  • ThreadLocal則幫咱們提供了另一種思路,就是在每個線程中保留副本,就是上文有提到的以空間換時間的形式保證資源的合理有序使用,因此我以爲也是解決併發問題的一種思路.

  • 第一次在掘金髮文章。。也是第一次在網上發文章 有點小緊張
  • 這是個人我的博客CheNbXxx,目前只有一些源碼的閱讀筆記,和讀書筆記.
  • 有空會優化一下一些筆記格式再上傳,感謝瀏覽.
相關文章
相關標籤/搜索