【JAVA併發編程系列】ThreadLocal

【JAVA併發編程系列】ThreadLocal

【1】ThreadLocal 類結構與關鍵屬性

//ThreadLocal 定義類時帶有泛型,
//說明 ThreadLocal 可以儲存任意格式的數據
//ThreadLocal 類是泛型的,可以放任意值
public class ThreadLocal<T>

//threadLocalHashCode 表示當前 ThreadLocal 的 hashCode
//用於計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();

// 計算 ThreadLocal 的 hashCode 值(就是遞增)
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

//HASH_INCREMENT 轉化爲十進制是 1640531527,
//2654435769 轉換成 int 類型就是 -1640531527,
//2654435769 等於 (√5-1)/2 乘以 2 的 32 次方
//(√5-1)/2 就是黃金分割數,近似爲 0.618
//0x61c88647 理解爲一個黃金分割數乘以 2 的 32 次方,
//它可以保證 nextHashCode 生成的哈希值,均勻的分佈在 2 的冪次方上,
//且小於 2 的 32 次方
private static final int HASH_INCREMENT = 0x61c88647;

// nextHashCode 主要作用是當前 ThreadLocal 賦唯一值,計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置
// 被 static 修飾非常關鍵,因爲一個線程在處理業務的過程中,ThreadLocalMap 是會被 set 多個 ThreadLocal 的,
// 多個 ThreadLocal 就依靠 threadLocalHashCode 進行區分
// static + AtomicInteger 保證了在一臺機器上每個 ThreadLocal 的 threadLocalHashCode 是唯一的
private static AtomicInteger nextHashCode =
    new AtomicInteger();

【2】ThreadLocalMap

ThreadLocalMap 本身就是一個簡單的 Map 結構,key 是 ThreadLocal,value 是 ThreadLocal 保存的值,底層是數組的數據結構

爲什麼使用弱引用

假設 threadLocal 使用的是強引用,在業務代碼中執行 threadLocalInstance=null 操作,以實現清理掉 threadLocal 實例的目的,但是因爲 threadLocalMap 的 Entry 強引用 threadLocal,因此在 gc 的時候進行可達性分析,threadLocal 依然可達,對 threadLocal 並不會進行垃圾回收,這樣就無法真正達到業務邏輯的目的;

// 數組中的每個節點值,WeakReference 是弱引用,當沒有引用指向時,會直接被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
    // 當前 ThreadLocal 關聯的值
    Object value;
    // WeakReference 的引用 referent 就是 ThreadLocal
    // Entry 的 Key 是 ThreadLocal
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

// 數組的初始化大小
private static final int INITIAL_CAPACITY = 16;

// 存儲 ThreadLocal 的數組
private Entry[] table;

// 擴容的閾值, 默認是數組大小的三分之二
private int threshold;

//ThreadLocalMap : 構造函數
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap 解決散列衝突的方法,開放定址法,原因,在 ThreadLocalMap 中的散列值分散的十分均勻,很少會出現衝突並且 ThreadLocalMap 經常需要清除無用的對象,使用純數組更加方便;

【3】ThreadLocal 是如何做到線程之間數據隔離的

ThreadLocalMap 是線程的屬性,所以每個線程的 ThreadLocals 都是隔離獨享的,父線程在創建子線程的情況下,會拷貝 inheritableThreadLocals 的值,但不會拷貝 threadLocals 的值;

【4】ThreadLocal 的 set 方法

// set 方法的主要作用是往當前 ThreadLocal 裏面設置值
// set 操作每個線程都是串行的,不會有線程安全的問題
public void set(T value) {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取當前線程的 ThreadLocalMap 對象
    ThreadLocalMap map = getMap(t);
    // 當前 thradLocal 之前有設置值,直接設置,否則初始化
    // map 存在則設置值
    if (map != null)
        map.set(this, value);
    else
        // 初始化ThreadLocalMap
        createMap(t, value);
}

//返回對應線程的 ThreadLocalMap 對象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    //初始化線程對象的 threadLocals 成員變量
    //this : 當前 ThreadLocal 類實例
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

 ThreadLocalMap 的 set 方法

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    // 計算 key 在數組中的下標
    //根據threadLocal的hashCode確定Entry應該存放的位置
    int i = key.threadLocalHashCode & (len-1);

    //採用開放地址法,hash衝突的時候使用線性探測
    // 查看 i 索引位置有沒有值,有值的話,索引位置 + 1,直到找到沒有值的位置
    // 這種解決 hash 衝突的策略,也導致了其在 get 時查找策略有所不同,體現在 getEntryAfterMiss 中
    for (Entry e = tab[i];
            e != null;
            // nextIndex 就是讓在不超過數組長度的基礎上,把數組的索引位置 + 1
            e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 找到內存地址一樣的 ThreadLocal,直接替換
        if (k == key) {
            e.value = value;
            return;
        }
        // 當前 key 是 null,說明 ThreadLocal 被清理了,直接替換掉
        //當key爲null時,說明threadLocal強引用已經被釋放掉,那麼就無法
        //再通過這個key獲取threadLocalMap中對應的entry,這裏就存在內存泄漏的可能性
        if (k == null) {
            //用當前插入的值替換掉這個key爲null的“髒”entry(stale entry)
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 當前 i 位置是無值的,可以被當前 thradLocal 使用
    //新建entry並插入table中i處
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //插入後再次清除一些key爲null的“髒”entry(stale entry),如果大於閾值就需要擴容
    // 當數組大小大於等於擴容閾值(數組大小的三分之二)時,進行擴容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMap Entry 數組擴容

//擴容
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();
            //遍歷過程中如果遇到髒entry的話直接令value爲null,
            //使得value能夠被回收,解決隱藏的內存泄漏的問題
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // 計算 ThreadLocal 在新數組中的位置
                int h = k.threadLocalHashCode & (newLen - 1);
                // 如果索引 h 的位置值不爲空,往後+1,直到找到值爲空的索引位置
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                // 給新數組賦值
                newTab[h] = e;
                count++;
            }
        }
    }
    // 給新數組初始化下次擴容閾值,爲數組長度的三分之二
    setThreshold(newLen);
    size = count;
    table = newTab;
}

【5】ThreadLocal 的 get 方法

public T get() {
    //獲取當前線程的實例對象
    //因爲 threadLocal 屬於線程的屬性,所以需要先把當前線程拿出來
    Thread t = Thread.currentThread();
    //獲取當前線程的threadLocalMap
    //從線程中拿到 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //獲取map中當前threadLocal實例爲key的值的entry
        //從 map 中拿到 entry,由於 ThreadLocalMap 在 set 時的 hash 衝突的策略不同,
        //導致拿的時候邏輯也不太一樣
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果不爲空,讀取當前 ThreadLocal 中保存的值
        if (e != null) {
            @SuppressWarnings("unchecked")
            //當前entitiy不爲null的話,就返回相應的值value
            T result = (T)e.value;
            return result;
        }
    }
    //若map爲null或者entry爲null的話通過該方法初始化,並返回該方法返回的value
    //否則給當前線程的 ThreadLocal 初始化,並返回初始值 null
    return setInitialValue();
}
private T setInitialValue() {
    //initialValue() : 方法是protected修飾的,
    //即繼承ThreadLocal的子類可重寫該方法,實現賦值爲其他的初始值
    T value = initialValue();
    //獲取當前線程的實例對象
    Thread t = Thread.currentThread();
    //獲取當前線程的threadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //map 存在則設置值
        map.set(this, value);
    else
        //初始化ThreadLocalMap
        createMap(t, value);
    //返回初始化值
    return value;
}

ThreadLocalMap 的 getEntry 方法

// 得到當前 thradLocal 對應的值,值的類型是由 thradLocal 的泛型決定的
// 由於 thradLocalMap set 時解決 Hash 衝突的邏輯,導致 thradLocalMap get 時的邏輯也會相應不同
// 首先嚐試根據 hashcode 取模數組大小 = 索引位置i 尋找,找不到的話,自旋把 i+1,直到找到 thradLocal
private Entry getEntry(ThreadLocal<?> key) {
    // 計算索引位置:ThreadLocal 的 hashCode 取模數組大小
    int i = key.threadLocalHashCode & (table.length - 1);
    // 根據索引i獲取entry
    Entry e = table[i];
    // e 不爲空,並且 e 的 ThreadLocal 的內存地址和 key 相同,直接返回,
    // 否則就是沒有找到,繼續通過 getEntryAfterMiss 方法找
    if (e != null && e.get() == key)
        return e;
    else
        // 未查找到滿足條件的entry,額外在做的處理
        return getEntryAfterMiss(key, i, e);
}

ThreadLocalMap 的 getEntryAfterMiss 方法

// 自旋 i+1,直到找到爲止
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 在大量使用不同 key 的 ThreadLocal 時,其實還蠻耗性能的
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 內存地址一樣,表示找到了
        if (k == key)
            //找到和查詢的key相同的entry則返回
            return e;
        // 刪除沒用的 key
        if (k == null)
            //解決髒entry的問題
            expungeStaleEntry(i);
        else
            // 繼續使索引位置 + 1
            // 繼續向後環形查找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

【6】ThreadLocal 的 remove 方法

public void remove() {
    //獲取當前線程的threadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
    //從map中刪除以當前threadLocal實例爲key的鍵值對
        m.remove(this);
}

ThreadLocalMap 的 remove 方法

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //向後環形查找與 key 對應的 Entry
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //將entry的key置爲null
            e.clear();
            //將該entry的value也置爲null
            expungeStaleEntry(i);
            return;
        }
    }
}

【7】ThreadLocal 內存泄漏問題

【7.1】ThreadLocal 內存泄漏產生的原因

threadLocal threadLocalMap entry 之間的關係圖示

上圖中,實線代表強引用,虛線代表的是弱引用,如果threadLocal外部強引用被置爲null(threadLocalInstance=null)的話,threadLocal實例就沒有一條引用鏈路可達,很顯然在gc(垃圾回收)的時候勢必會被回收,因此entry就存在key爲null的情況,無法通過一個Key爲null去訪問到該entry的value。同時,就存在了這樣一條引用鏈:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,導致在垃圾回收的時候進行可達性分析的時候,value可達從而不會被回收掉,但是該value永遠不能被訪問到,這樣就存在了內存泄漏。

【7.2】ThreadLocal 內存泄漏的解決

ThreadLocalMap 的 cleanSomeSlots 方法

//參數 n 作用 :
//掃描控制 (scan control),從while中是通過n來進行條件判斷的,
//說明n就是用來控制掃描趟數(循環次數)的
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;
            //遇到“髒”entry(stale entry)
            //調用 expungeStaleEntry 函數處理
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

ThreadLocalMap 的 expungeStaleEntry 方法

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

    //清除當前髒entry
    // expunge entry at staleSlot
    // value置爲null後該value域變爲不可達,在下一次gc的時候就會被回收掉
    // table[staleSlot]爲null後以便於存放新的entry
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    //往後環形繼續查找,直到遇到table[i]==null時結束
    for (i = nextIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            //遇到 k 爲 null 則清除當前髒 entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //處理rehash的情況
            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;
}

ThreadLocalMap 的 replaceStaleEntry 方法

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).
    //向前找到第一個髒entry
    //現髒entry的相鄰位置也有很大概率出現髒entry,
    //所以爲了一次處理到位,就需要向前環形搜索,找到前面的髒entry
    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.
        if (k == key) {
            //如果在向後環形查找過程中發現key相同的entry就覆蓋並且和髒entry進行交換
            e.value = value;

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

            // Start expunge at preceding stale entry if it exists
            //如果在查找過程中還未發現髒entry,那麼就以當前位置作爲cleanSomeSlots的起點
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            //搜索髒entry並進行清理
            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.
        //如果向前未搜索到髒entry,則在查找過程遇到髒entry的話,
        //後面就以此時這個位置作爲起點執行cleanSomeSlots
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    //如果在查找過程中沒有找到可以覆蓋的entry則將新的entry插入在髒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
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

replaceStaleEntry 處理過程圖示

1. 前向有髒 entry 後向環形查找找到可覆蓋的 entry

2. 前向有髒 entry 後向環形查找未找到可覆蓋的 entry

3. 前向沒有髒 entry 後向環形查找找到可覆蓋的 entry

4. 前向沒有髒 entry 後向環形查找未找到可覆蓋的 entry

ThreadLocalMap 的 getEntryAfterMiss 方法

// 自旋 i+1,直到找到爲止
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 在大量使用不同 key 的 ThreadLocal 時,其實還蠻耗性能的
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 內存地址一樣,表示找到了
        if (k == key)
            //找到和查詢的key相同的entry則返回
            return e;
        // 刪除沒用的 key
        if (k == null)
            //解決髒entry的問題
            expungeStaleEntry(i);
        else
            // 繼續使索引位置 + 1
            // 繼續向後環形查找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

ThreadLocalMap 的 remove 方法

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //向後環形查找與 key 對應的 Entry
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //將entry的key置爲null
            e.clear();
            //將該entry的value也置爲null
            expungeStaleEntry(i);
            return;
        }
    }
}

 

參考致謝

本博客爲博主學習筆記,同時參考了網上衆博主的博文以及相關專業書籍,在此表示感謝,本文若存在不足之處,請批評指正。

【1】不懂ThreadLocal,面試官很難相信你懂Java併發編程

【2】ThreadLocal

【3】JAVA併發專題十七:深入理解ThreadLocal(一)

【4】JAVA併發系列十八:深入理解ThreadLocal(二)

【5】JAVA併發系列十九:深入理解ThreadLocal(三)–詳解ThreadLocal內存泄漏問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章