ThreadLocal源碼分析

簡介

ThreadLocal是每一個線程本身維護的一個存儲對象的數據結構,線程間互不影響實現線程封閉。通常咱們經過ThreadLocal對象的get/set方法存取對象。數組

源碼分析

這裏以Java8爲例bash

ThreadLocal的set方法源碼以下數據結構

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 根據當前線程得到ThreadLocalMap對象
    if (map != null)
        map.set(this, value); // 若是有則set
    else
        createMap(t, value); // 不然建立ThreadLocalMap對象
}
    
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
    
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製代碼

經過getMap方法,可見咱們返回的map其實是Thread對象的threadLocals屬性。而這個ThreadLocalMap就是用來存儲數據的結構。源碼分析

ThreadLocalMap介紹

ThreadLocalMap是ThreadLocal的核心,定義在ThreadLocal類裏的內部類,他維護了一個Enrty數組。ThreadLocal存/取數據都是經過操做Enrty數組來實現的。ui

Enrty數組做爲一個哈希表,將對象經過開放地址方法散列到這個數組中。做爲對比,HashMap則是經過鏈表法將對象散列到數組中。this

開放地址法就是元素散列到數組中的位置若是有衝突,再以某種規則在數組中找到下一個能夠散列的位置,而在ThreadLocalMap中則是使用線性探測的方式向後依次查找能夠散列的位置。spa

Enery介紹

Enery在這裏咱們稱之爲元素,是散列表中維護的對象單元。線程

// 哈希表中的元素使用其引用字段做爲鍵(它始終是ThreadLocal對象)並繼承WeakReference。
// 注意,null鍵(即entry.get()== null)表示再也不引用該鍵,所以能夠從表中刪除該元素。
// null鍵對應的這些元素在下面的代碼中稱爲「舊元素」。
// 這些「舊元素」就是髒對象,但由於存在線程的引用不會被GC,後面會詳說
// 爲避免內存泄露須要代碼裏清理,將引用置爲null,那麼這些對象以後就會被GC清理。
// 實際上後面的代碼很大程度上都是在描述如何清理「舊元素」的引用
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
複製代碼

到這裏可能有兩個疑問code

一、既然要存儲的內容是線程獨有的對象,爲何不直接在Thread裏設置一個屬性直接存儲該對象?或者說爲何要維護一個Entry散列表來存儲內容並以ThreadLocal對象做爲key?對象

答:一個ThreadLocal對象只屬於一個線程,但一個線程能夠實例化ThreadLocal對象。而ThreadLocalMap維護的數組存儲的就是以ThreadLocal實例做爲key的Entry對象。

二、ThreadLocalMap中的Enery爲何要繼承WeakReference?

答:首先弱引用會在ThreadLocal對象不存在強引用的狀況,弱引用對象會在下次GC時被清除。 將ThreadLocal對象做爲弱引用目的是爲了防止內存泄露。

假設Enery的key不是弱引用,即便在咱們的代碼裏threadLocal引用已失效,threadLocal也不會被GC,由於當前線程持有ThreadLocalMap的引用,而ThreadLocalMap持有Entry數組的引用,Entry對象的key又持有threadLocal的引用,threadLocal對象針對當前線程可達,因此不會被GC。

而Enery的key值threadLocal做爲弱引用,在引用失效時會被GC。但即便threadLocal作爲弱引用被GC清理,Entry[]仍是存在entry對象,只是key爲null,vlue對象也還存在,這些都是髒對象。弱引用不單是清理了threadLocal對象,它的另外一層含義是能夠標識出Enery[]數組中哪些元素應該被GC(咱們這裏稱爲舊元素),而後程序裏找出這些entry並清理。

ThreadLocalMap的set方法

回到前面提到的set方法,當map不爲null時會調用ThreadLocalMap的set方法。

ThreadLocalMap的set方法描述瞭如何將值散列到哈希表中,是開放地址法以線性探測方式散列的實現。在成功set值以後,嘗試清理一些舊元素,若是沒有發現舊元素則判斷閾值,確認哈希表是否足夠大、是否須要擴容。若是哈希表過於擁擠,get/set值會發生頻繁的衝突,這是不指望的狀況。ThreadLocalMap的set方法代碼及詳細註釋以下

private void set(ThreadLocal<?> key, Object value) {
    // We do not 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.
    // 咱們不像get()那樣先使用快速路徑(直接散列)判斷
    // 由於使用set()建立新元素至少與替換現有元素同樣頻繁,在這種狀況下,散列後馬上判斷會容易失敗。
    // 因此直接先線性探測
    Entry[] tab = table;
    int len = tab.length;

    // 根據hashcode散列到數組位置
    int i = key.threadLocalHashCode & (len-1);
    // 開放地址法處理散列衝突,線性探測找到能夠存放位置
    // 遍歷數組找到下一個能夠存放元素的位置,這種位置包含三種狀況
    // 1.元素的key已存在,直接賦值value
    // 2.元素的key位null,說明k做爲弱引用被GC清理,該位置爲舊數據,須要被替換
    // 3.直到遍歷到一個數組位置爲null的位置賦值
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {//key已存在則直接更新
            e.value = value;
            return;
        }
        if (k == null) { //e不爲null但k爲null說明k做爲弱引用被GC,是舊數據須要被清理
            // i爲舊數據位置,清理該位置並依據key合理地散列或將value替換到數組中
            // 而後從新散列i後面的元素,並順便清理i位置附近的其餘舊元素
            // replaceStaleEntry方法在後面會詳細分析
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 遍歷到一個數組位置爲null的位置賦值
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 調用cleanSomeSlots嘗試性發現並清理舊元素,若是沒有發現且舊元素當前容量超過閾值,則調用rehash
    // cleanSomeSlots在後面會詳細分析
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 此時認爲表空間不足,全量遍歷清理舊元素,清理後判斷容量若大於閾值的3/4,如果則擴容並重新散列
        // rehash在後面會詳細分析
        rehash();
}
複製代碼

replaceStaleEntry方法

replaceStaleEntry方法是當咱們線性探測時,若是碰到了舊元素就執行。該方法作的事情比較多,能夠總結爲咱們在staleSlot位置發現舊元素,將新值覆蓋到staleSlot位置上並清理staleSlot附近的舊元素。「附近」指的是staleSlot位置先後連續的非null元素。代碼及詳細註釋以下

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).
    // 向前檢查是否存在舊元素,一次性完全清理因爲GC清除的弱引用key致使的舊數據,避免屢次執行
    int slotToExpunge = staleSlot;
    // 向前遍歷找到entry不爲空且key爲null的位置賦值給slotToExpunge
    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
    // staleSlot位置向後遍歷若是位置不爲空,判斷key是否已經存在
    // 回想前面咱們是set實例的時候,碰到舊元素的狀況下調用該方法,因此極可能在staleSlot後面key是已經存在的
    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.
        // 若是咱們找到鍵,那麼咱們須要將它與舊元素交換以維護哈希表順序。
        // 而後能夠將交換後獲得的舊索引位置
        // 或其上方遇到的任何其餘舊索引位置傳給expungeStaleEntry清理舊條
        // 若是碰到key相同的值則覆蓋value
        if (k == key) {
            e.value = value;
            // i位置與staleSlot舊數據位置作交換,將數組元素位置規範化,維護哈希表順序
            // 這裏維護哈希表順序是必要的,舉例來講,回想前面threadLocal.set實例的判斷,是線性探測找到能夠賦值的位置
            // 若是哈希順序不維護,可能形成同一個實例被賦值屢次的狀況
            // 包括後面清理舊元素的地方都要從新維護哈希表順序
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            // Start expunge at preceding stale entry if it exists
            // 開始清理前面的舊元素
            // 若是前面向前或向後查找的舊元素不存在,也就是slotToExpunge == staleSlot
            //此時slotToExpunge = i,此時位置i的元素是舊元素,須要被清理
            // slotToExpunge用來存儲第一個須要被清理的舊元素位置
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 清理完slotToExpunge位置及其後面非空連續位置後,經過調用cleanSomeSlots嘗試性清理一些其餘位置的舊元素
            // cleanSomeSlots不保證清理所有舊元素,它的時間複雜度O(log2n),他只是全量清理舊元素或不清理的折中
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we do not find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        // 若是前面向前查找的舊元素不存在,也就是slotToExpunge == staleSlot,而此時位置i爲舊元素,因此將i賦值給slotToExpunge
        // slotToExpunge用來存儲第一個須要被清理的舊元素位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    // 若是向後遍歷非空entry都沒有找到key,則直接賦值給當前staleSlot舊元素位置
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    // 經過前面根據staleSlot向前/向後遍歷,若是發現有舊元素則清理
    if (slotToExpunge != staleSlot)
        // 清理完slotToExpunge位置及其後面非空連續位置後,經過調用cleanSomeSlots嘗試性清理一些其餘位置的舊元素
        // cleanSomeSlots不保證清理所有舊元素,它的時間複雜度O(log2n),他只是全量清理舊元素或不清理的折中
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
複製代碼

expungeStaleEntry方法

查找到的舊元素都會執行expungeStaleEntry方法。expungeStaleEntry頻繁被使用,它是清理舊元素的核心方法。該方法的作的事情就是:清理包括staleSlot位置後面連續爲空元素中的全部舊元素並從新散列,返回staleSlot後面首個null位置。代碼及詳細註釋以下

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    // 清空staleSlot位置的元素
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    // 舊位置清理後,後面的元素須要從新散列到數組裏,直到遇到數組位置爲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) { // k == null說明此位置也是舊數據,須要清理
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            // 將staleSlot後面不爲空位置從新散列,若是與當前位置不一樣,則向前移動到h位置後面(包括h)的首個空位置
            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;
}
複製代碼

cleanSomeSlots方法

cleanSomeSlots是一個比較靈動的方法。就如他的名字"some"同樣。該方法只是嘗試性地尋找一些舊元素。添加新元素或替換舊元素時都會調用此方法。它的執行復雜度log2(n),他是 「不清理」和「全量清理」的折中。如有發現舊元素返回true。代碼及詳細註釋以下

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);
        }
    // n >>>= 1無符號右移1位,即移動次數以n的二進制最高位的1的位置爲基準
    // 因此時間複雜度log2(n)
    } while ( (n >>>= 1) != 0);
    return removed;
}
複製代碼

rehash/expungeStaleEntries/resize方法

在成功set值後,經過閾值判斷,若是程序認爲表空間不足就會調用rehash方法。 rehash作了兩件事,首先全量遍歷清理舊元素,而後在清理後判斷容量是否足夠,若成立則2倍擴容並從新散列。 expungeStaleEntries則是全量清理舊元素,resize則是二倍擴容。

// rehash全量地遍歷清理舊元素,而後判斷容量若大於閾值的3/4,則擴容並重新散列
// 程序認爲表空間不足時會調用該方法
private void rehash() {
    // 全量遍歷清理舊元素
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    // 適當的擴容,以免hash散列到數組時過多的位置衝突
    if (size >= threshold - threshold / 4)
        // 2倍擴容並從新散列
        resize();
}

// 全量遍歷清理舊元素
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

// 二倍擴容
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();
            if (k == null) {
                e.value = null; // Help the GC
            } 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的get邏輯相比set要簡單的多。他只是將threadLocal對象散列到數組中,經過線性探測的方式找到匹配的值。代碼及詳細註釋以下

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;
        }
    }
    // 若是map不爲null初始化一個key爲當前threadLocal值爲null的ThreadLocalMap對象
    return setInitialValue();
}

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 // 直接散列找不到的狀況,調用getEntryAfterMiss線性探測查找指望元素
        return getEntryAfterMiss(key, i, e);
}

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

remove方法

remove即將引用清空並調用清理舊元素方法。因此remove不會產生舊元素,當咱們確認哪些內容須要移除時優先使用remove方法清理,儘可能不要交給GC處理。避免get/set發現舊元素的狀況過多。

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

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

總結

ThreadLocal最大的複雜性在於如何處理舊元素,目的是爲了處理已泄露的內存(對象已經不被使用但卻沒法被GC)。

在新增或替換元素成功後,爲了儘量少地在get/set時發現有舊元素的狀況,在清理舊元素後屢次調用cleanSomeSlots嘗試性地發現並清理一些舊元素,爲了執行效率,「cleanSome」是「no clean」 不清理和「clean all」全量清理之間一的種平衡。

expungeStaleEntry在清理本身位置上的舊元素的同時也會清理附近的舊元素,爲得都是減小get/set發現舊元素的狀況。即使如此,在哈希表容量過多時也會全量清理一遍舊元素並擴容。

當確認元素須要清除時,優先使用remove方法。

相關文章
相關標籤/搜索