ThreadLocal 解析

簡單使用

//咱們只須要實例化一次,做爲key就夠了,或者使用靜態。
private  ThreadLocal myThreadLoca<String>l = new ThreadLocal();

// 寫入
myThreadLocal.set("A thread local value");

// 讀取
String threadLocalValue = myThreadLocal.get();
複製代碼

ThreadLocal 與Thread 同步機制的比較

同步機制 -- 「以時間換空間」

在同步機制中,經過對象的鎖機制保證同一時間只有一個線程訪問變量,實現串行化.這時該變量是多個線程共享的,使用同步機制要求程序縝密地分析何時對變量進行讀/寫、何時須要鎖定某個對象、何時釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。java

ThreadLocal -- 「以空間換時間」

給每一個線程創建副本,互不干擾.數組

總結

對於多線程資源共享的問題.安全

同步機制採用「以時間換空間」的方式:訪問串行化,對象共享化 ; ThreadLocaI採用了「以空間換時間」的方式:訪問並行化,對象獨享化。數據結構

前者僅提供一份變量,讓不一樣的線程排隊訪問;然後者爲每一個線程都提供了一份變量,所以能夠同時訪問而互不影響。多線程

ThreadLocal 源碼分析

ThreadLocal的Hash值的計算

// hash code
private final int threadLocalHashCode = nextHashCode();
 
// AtomicInteger類型,從0開始
private static AtomicInteger nextHashCode =
    new AtomicInteger();
 
// hash code每次增長1640531527 
private static final int HASH_INCREMENT = 0x61c88647;
 
// 下一個hash code
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
複製代碼

ThreadLocal的hashcode(threadLocalHashCode)是從0開始,每新建一個ThreadLocal,對應的hashcode就加0x61c88647.工具

ThreadLocal的set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 獲取當前線程的ThreadLocalMap
    // 當前線程的ThreadLocalMap不爲空則調用set方法, this爲調用該方法的ThreadLocal對象
    if (map != null) 
        map.set(this, value);
    // map爲空則調用createMap方法建立一個新的ThreadLocalMap, 並新建一個Entry放入該
    // ThreadLocalMap, 調用set方法的ThreadLocal和傳入的value做爲該Entry的key和value
    else
        createMap(t, value);    
}
 
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;	// 返回線程t的threadLocals屬性,類型是ThreadLocalMap
}
複製代碼
  1. 先拿到當前線程,再使用getMap方法拿到當前線程的threadLocals變量(類型ThreadLocalMap)
  2. 若是threadLocals不爲空,則將當前ThreadLocal做爲key,傳入的值做爲value,調用set方法(見下文ThreadLocalMap的set方法)插入threadLocals。
  3. 若是threadLocals爲空則調用建立一個ThreadLocalMap,並新建一個Entry放入該ThreadLocalMap, 調用set方法的ThreadLocal自身和傳入的value做爲該Entry的key和value

注意此處的threadLocals變量是一個ThreadLocalMap,是Thread的一個局部變量,所以它只與當前線程綁定。源碼分析

ThreadLocal的get方法

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

也是先得到當前線程的ThreadLocalMap變量.其實ThreadLocal類就像是一個工具類同樣,核心是其內部類ThreadLocalMap.this

ThreadLocalMap 源碼分析

示意圖

ThreadLocalMap是一個自定義哈希映射,僅用於維護線程本地變量值。
ThreadLocalMap是ThreadLocal的內部類,主要有一個Entry數組,Entry的key爲ThreadLocal,value爲ThreadLocal對應的值。
每一個線程都有一個ThreadLocalMap類型的threadLocals變量。spa

成員變量和相關方法

// 初始容量16
private static final int INITIAL_CAPACITY = 16;
// entry數組
private Entry[] table;
// entry 元素個數
private int size = 0;
// 擴容閥值
private int threshold; // Default to 0
// 一次set是2/3 len
private void setThreshold(int len) {  threshold = len * 2 / 3;  }

//下面兩個方法能夠向前/後獲取座標.
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0);}

private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1); }
				
複製代碼

Entry實體

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
 
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
複製代碼

結合示意看,entry中的key是一個弱引用.它的存在不會使得ThreadLocal變量豁免垃圾回收.當定義ThreadLocal變量的線程終止,沒有強引用關聯ThreadLocal變量,它就會被垃圾回收..net

說到底ThreadLocal變量只是一個key而已,能夠用它去各個線程本身的ThreadLocalMap中查詢,它自己並不存儲信息.

固然這會形成各個線程的ThreadLocalMap中出現key爲null的entry.這個在後邊內存泄漏會講到.

擴容

private void rehash() {
    expungeStaleEntries();  // 調用expungeStaleEntries方法清理key爲null的Entry
 
    // 若是清理後size超過閾值的3/4, 則進行擴容
    if (size >= threshold - threshold / 4)  
		    // 大小*2 
				// h = k.threadLocalHashCode & (newLen - 1);
        resize();
}
複製代碼

清理key爲null的entry

cleanSomeSlots方法

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) {	// 遍歷到key爲null的元素
            n = len;	// 重置n的值
            removed = true;	// 標誌有移除元素
            i = expungeStaleEntry(i);	// 移除i位置及以後的key爲null的元素
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
複製代碼

從 i 開始,清除key爲空的Entry,掃描次數是log2n,當遍歷到一個key爲null的元素時,調用expungeStaleEntry清除,並將遍歷次數重置。 n的值多是當前元素個數size(從增長元素操做調過來).或者整個entry長度len(從replaceStaleEntry方法調過來). 官方給出的解釋是這個方法簡單、快速,而且效果不錯。

expungeStaleEntry 方法

// 從staleSlot開始, 清除key爲空的Entry, 並將不爲空的元素放到合適的位置,最後返回Entry爲空的位置
private int expungeStaleEntry(int staleSlot) {  
    Entry[] tab = table;
    int len = tab.length;
 
    // expunge entry at staleSlot
    tab[staleSlot].value = null;    // 將tab上staleSlot位置的對象清空
    tab[staleSlot] = null;
    size--;
 
    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); // 遍歷下一個元素, 即(i+1)%len位置的元素
         (e = tab[i]) != null;	// 遍歷到Entry爲空時, 跳出循環並返回索引位置
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get(); 
        if (k == null) {    // 當前遍歷Entry的key爲空, 則將該位置的對象清空
            e.value = null;
            tab[i] = null;
            size--;
        } else {    // 當前遍歷Entry的key不爲空
            int h = k.threadLocalHashCode & (len - 1);  // 從新計算該Entry的索引位置
            if (h != i) {   // 若是索引位置不爲當前索引位置i
                tab[i] = null;  // 則將i位置對象清空, 替當前Entry尋找正確的位置
 
                // 若是h位置不爲null,則向後尋找當前Entry的位置
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;   
}
複製代碼

ThreadLocalMap的set方法

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);  // 計算出索引的位置
 
    // 從索引位置開始遍歷,因爲不是鏈表結構,所以經過nextIndex方法來尋找下一個索引位置 
    for (Entry e = tab[i];
         e != null;	// 當遍歷到的Entry爲空時結束遍歷
         e = tab[i = nextIndex(i, len)]) {  
        ThreadLocal<?> k = e.get(); // 拿到Entry的key,也就是ThreadLocal 
 
        // 該Entry的key和傳入的key相等, 則用傳入的value替換掉原來的value 
        if (k == key) {
            e.value = value;
            return;
        }
 
        // 該Entry的key爲空, 則表明該Entry須要被清空, 
        // 調用replaceStaleEntry方法 
        if (k == null) {
        	// 該方法會繼續尋找傳入key的安放位置, 並清理掉key爲空的Entry 
            replaceStaleEntry(key, value, i);
            return;
        }
    }
 
    // 尋找到一個空位置,&emsp;則放置在該位置上
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // cleanSomeSlots是用來清理掉key爲空的Entry,若是此方法返回true,則表明至少清理
    // 了1個元素, 則這次set必然不須要擴容, 若是此方法返回false則判斷sz是否大於閾值
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();   // 擴容
}
複製代碼
  1. 經過傳入的key的hashCode計算出索引的位置
  2. 從索引位置開始遍歷,因爲不是鏈表結構,所以經過nextIndex方法來尋找下一個索引位置
  3. 若是找到某個Entry的key和傳入的key相同,則用傳入的value替換掉該Entry的value。
  4. 若是遍歷到某個Entry的key爲空,則調用replaceStaleEntry方法
  5. 若是經過nextIndex尋找到一個空位置(表明沒有找到key相同的),則將元素放在該位置上
  6. 調用cleanSomeSlots方法清理key爲null的Entry,並判斷是否須要擴容,若是須要則調用rehash方法進行擴容。

replaceStaleEntry 方法

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
 
    int slotToExpunge = staleSlot;	// 清除元素的開始位置(記錄索引位置最前面的)
    // 向前遍歷,直到遇到Entry爲空
    for (int i = prevIndex(staleSlot, len); 
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;	// 記錄最後一個key爲null的索引位置
 
    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len); // 向後遍歷,直到遇到Entry爲空
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
 
        // 該Entry的key和傳入的key相等, 則將傳入的value替換掉該Entry的value
        if (k == key) {
            e.value = value;
 
            // 將i位置和staleSlot位置的元素對換(staleSlot位置較前,是要清除的元素)
            tab[i] = tab[staleSlot];	
            tab[staleSlot] = e;
 
            // 若是相等, 則表明上面的向前尋找key爲null的遍歷沒有找到,
            // 即staleSlot位置前面的元素沒有須要清除的,此時將slotToExpunge設置爲i, 
            // 由於原staleSlot的元素已經被放到i位置了,這時位置i前面的元素都不須要清除
            if (slotToExpunge == staleSlot) 
                slotToExpunge = i;
            // 從slotToExpunge位置開始清除key爲空的Entry
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
 
        // 若是第一次遍歷到key爲null的元素,而且上面的向前尋找key爲null的遍歷沒有找到,
        // 則將slotToExpunge設置爲當前的位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
 
    // 若是key沒有找到,則新建一個Entry,放在staleSlot位置
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
 
    // 若是slotToExpunge!=staleSlot,表明除了staleSlot位置還有其餘位置的元素須要清除
    // 須要清除的定義:key爲null的Entry,調用cleanSomeSlots方法清除key爲null的Entry
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
複製代碼
  1. slotToExpunge始終記錄着須要清除的元素的最前面的位置(即slotToExpunge前面的元素是不須要清除的)
  2. 從位置staleSlot向前遍歷,直到遇到Entry爲空,用staleSlot記錄最後一個key爲null的索引位置(也就是遍歷過位置最前的key爲null的位置)
  3. 從位置staleSlot向後遍歷,直到遇到Entry爲空,若是遍歷到key和入參key相同的,則將入參的value替換掉該Entry的value,並將i位置和staleSlot位置的元素對換(staleSlot位置較前,是要清除的元素),遍歷的時候判斷slotToExpunge的值是否須要調整,最後調用expungeStaleEntry方法和cleanSomeSlots方法清除key爲null的元素。
  4. 若是key沒有找到,則使用入參的key和value新建一個Entry,放在staleSlot位置
  5. 判斷是否還有其餘位置的元素key爲null,若是有則調用expungeStaleEntry方法和cleanSomeSlots方法清除key爲null的元素

源碼總結

  1. 每一個線程都有一個ThreadLocalMap 類型的 threadLocals 屬性。
  2. ThreadLocalMap 類至關於一個Map,key 是 ThreadLocal 自己,value 就是咱們的值。
  3. 當咱們經過 threadLocal.set(new Integer(123)); ,咱們就會在這個線程中的 threadLocals 屬性中放入一個鍵值對,key 是 這個 threadLocal.set(new Integer(123))的threadlocal,value 就是值new Integer(123)。
  4. 當咱們經過 threadlocal.get() 方法的時候,首先會根據這個線程獲得這個線程的 threadLocals 屬性,而後因爲這個屬性放的是鍵值對,咱們就能夠根據鍵 threadlocal 拿到值。 注意,這時候這個鍵 threadlocal 和 咱們 set 方法的時候的那個鍵 threadlocal 是同樣的,因此咱們可以拿到相同的值。
  5. ThreadLocalMap 的get/set/remove方法跟HashMap的內部實現都基本同樣,經過 "key.threadLocalHashCode & (table.length - 1)" 運算式計算獲得咱們想要找的索引位置,若是該索引位置的鍵值對不是咱們要找的,則經過nextIndex方法計算下一個索引位置,直到找到目標鍵值對或者爲空。
  6. hash衝突:在HashMap中相同索引位置的元素以鏈表形式保存在同一個索引位置;而在ThreadLocalMap中,沒有使用鏈表的數據結構,而是將(當前的索引位置+1)對length取模的結果做爲相同索引元素的位置: 源碼中的nextIndex方法,能夠表達成以下公式:若是i爲當前索引位置,則下一個索引位置 = (i + 1 < len) ? i + 1 : 0

問題辨析

爲何ThreadLocal一般被static修飾?

ThreadLocal 是一個key,去各個線程本身的map裏取值,不必多個.

Java 中每一個線程都有與之關聯的Thread對象,Thread對象中有一個ThreadLocal.ThreadLocalMap類型的成員變量,該變量是一個Hash表.因此每一個線程都單獨維護這樣一個Hash表. 當ThreadLocal類型對象調用set方法時,這個set方法會使用當前線程維護的Hash表,把本身做爲key, set方法的參數做爲value插入到Hash表中.因爲每一個線程維護的Hash表是獨立的,所以在不一樣的Hash表中,key值即便相同也是沒問題的.

若是不使用static的ThreadLocal變量,那麼當定義ThreadLocal的類建立新的實例時候,會出現多個ThreadLocal.這在大多數時候是沒有意義的.

內存泄漏問題

ThreadLocal變量被正常回收

ThreadLocalMap使用ThreadLocal的弱引用做爲Entry的key,若是一個ThreadLocal沒有外部強引用來引用它,下一次系統GC時,這個ThreadLocal必然會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value。

咱們上面介紹的get、set、remove等方法中,都會對key爲null的Entry進行清除(expungeStaleEntry方法,將Entry的value清空,等下一次垃圾回收時,這些Entry將會被完全回收)。

可是若是當前線程一直在運行,而且一直不執行get、set、remove方法,這些key爲null的Entry的value就會一直存在一條強引用練:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value,致使這些key爲null的Entry的value永遠沒法回收,形成內存泄漏。

爲了不這種狀況,咱們能夠在使用完ThreadLocal後,手動調用remove方法,以免出現內存泄漏。

ThreadLocal變量沒被正常回收

因爲咱們通常把ThreadLocal變量聲明成static類型,延長了它的生命週期,若是因爲一些緣由,持有ThreadLocal變量聲明的類沒有被回收. 那麼確實全部使用這個ThreadLocal變量的線程中的ThreadLocalMap中的entry中的value都不會被回收(key != null). 這確實會形成問題,尤爲當你的工做線程是複用的,自己永遠不會終止,好比線程池/tomat的工做線程. 更糟糕的是,沒法回收的value對應的類自己,和它的類加載器也沒法被回收.這個類加載器全部加載的全部的類的數據會存留在永久區.形成永久區內存泄漏(permgen leak)

使用後remove()是一個好習慣.

InheritableThreadLocal

InheritableThreadLocal類是ThreadLocal類的子類. 子線程會copy父線程的值,造成本身的副本(淺拷貝).

1.對於可變對象:

  • 父線程初始化, 由於Thread Construct淺拷貝, 共用索引, 子線程修改父線程跟着變;
  • 父線程不初始化, 子線程初始化, 無Thread Construct淺拷貝, 子線程和父線程都是單獨引用, 不一樣對象, 子線程修改父線程不跟着變。

2.對於不可變對象: 不可變對象因爲每次都是新對象, 因此不管父線程初始化與否,子線程和父線程都互不影響。

從上面兩條結論可知,子線程只能經過修改可變性(Mutable)對象對主線程纔是可見的,即才能將修改傳遞給主線程,但這不是一種好的實踐,不建議使用,爲了保護線程的安全性,通常建議只傳遞不可變(Immuable)對象,即沒有狀態的對象。

參考

blog.csdn.net/v123411739/… www.zhihu.com/question/35…

相關文章
相關標籤/搜索