ThreadLocal與ThreadLocalMap原始碼分析

凝冰物語發表於2021-06-02

ThreadLocal類

該類主要用於不同執行緒儲存自己的執行緒本地變數。本文先通過一個示例簡單介紹該類的使用方法,然後從ThreadLocal類的初始化、儲存結構、增刪資料和hash值計算等幾個方面,分析對應原始碼。採用的版本為jdk1.8。

ThreadLocal-使用方法

ThreadLocal物件可以在多個執行緒中被使用,通過set()方法設定執行緒本地變數,通過get()方法獲取設定的執行緒本地變數。我們先通過一個示例簡單瞭解下使用方法:

public static void main(String[] args){
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    // 執行緒1
    new Thread(()->{
        // 檢視是否有初始值
        System.out.println("執行緒1的初始值:"+threadLocal.get());
        // 設定執行緒1的值
        threadLocal.set("V1");
        // 輸出
        System.out.println("執行緒1的值:"+threadLocal.get());
        // 等待一段時間,等執行緒2設定值後再檢視執行緒1的值
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行緒1的值:"+threadLocal.get());
    }).start();
    // 執行緒2
    new Thread(()->{
        // 等待執行緒1設定初始值
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 檢視執行緒2的初始值
        System.out.println("執行緒2的值:"+threadLocal.get());
        // 設定執行緒2的值
        threadLocal.set("V2");
        // 檢視執行緒2的值
        System.out.println("執行緒2的值:"+threadLocal.get());
    }).start();
}

由於threadlocal設定的值是在每個執行緒中都有一個副本的,執行緒之間不會互相影響。程式碼執行的結果如下所示:

執行緒1的初始值:null
執行緒1的值:V1
執行緒2的值:null
執行緒2的值:V2
執行緒1的值:V1

ThreadLocal-初始化

ThreadLocal類只有一個無參的構造方法,如下所示:

/**
 * Creates a thread local variable.
 * @see #withInitial(java.util.function.Supplier)
 */
public ThreadLocal() {
}

但其實還有一個帶引數的構造方法,不過是它的子類。ThreadLocal中定義了一個內部類SuppliedThreadLocal,為繼承自ThreadLocal類的子類。可以通過該類進行給定初始值的初始化,其定義如下:

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }

通過TheadLocal threadLocal = Thread.withInitial(supplier);這樣的語句可以進行給定初始值的初始化。在某個執行緒第一次呼叫get()方法時,會執行initialValue()方法設定執行緒變數為傳入supplier中的值。

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

ThreadLocal-儲存結構

在jdk1.8版本中,使用的是TheadLocalMap這一個容器儲存執行緒本地變數。

該容器的設計思想和HashMap有很多共同之處。比如:內部定義了Entry節點儲存鍵值對(使用ThreadLocal物件作為鍵);使用一個陣列儲存entry節點;設定一個閾值,超過閾值時進行擴容;通過鍵的hash值與陣列長度進行&操作確定下標索引等。但也有很多不同之處,具體我們在後續介紹ThreadLocalMap類時再詳細分析。

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

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    // 儲存元素的陣列    
    private Entry[] table;
    // 容器內元素數量
    private int size = 0;
    // 閾值
    private int threshold; // Default to 0
    // 修改和新增元素
    private void set(ThreadLocal<?> key, Object value){
        ...
    }
    // 移除元素
    private void remove(ThreadLocal<?> key) {
        ...
    }
    ...
}
    

ThreadLocal-增刪資料

ThreadLocal類提供了get(),set()和remove()方法來操作當前執行緒的threadlocal變數副本。底層則是基於ThreadLocalMap容器來實現資料操作。

不過要注意的是:ThreadLocal中並沒有ThreadLocalMap的成員變數,ThreadLocalMap物件是Thread類中的一個成員,所以需要通過通過當前執行緒的Thread物件去獲取該容器。

每一個執行緒Thread物件都會有一個map容器,該容器會隨著執行緒的終結而回收。

設定執行緒本地變數的方法。

public void set(T value) {
    // 獲取當前執行緒對應的Thread物件,其是map鍵值對中的健
    Thread t = Thread.currentThread();
    // 獲取當前執行緒物件的容器map
    ThreadLocalMap map = getMap(t);
    // 如果容器不為null,則直接設定元素。否則用執行緒物件t和value去初始化容器物件
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

// 通過當前執行緒的執行緒物件獲取容器
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 建立map容器,本質是初始化Thread物件的成員變數threadLocals
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

獲取執行緒本地變數的方法。

public T get() {
    // 獲取當前執行緒物件
    Thread t = Thread.currentThread();
    
    // 獲取當前執行緒物件的容器map
    ThreadLocalMap map = getMap(t);
    
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        
        // 如果容器不為null且容器內有當前threadlocal物件對應的值,則返回該值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    
    // 如果容器為null或者容器內沒有當前threadlocal物件繫結的值,則先設定初始值並返回該初始值
    return setInitialValue();
}

// 設定初始值。主要分為兩步:1.載入和獲取初始值;2.在容器中設定該初始值。
// 第二步其實和set(value)方法實現一模一樣。
private T setInitialValue() {

    // 載入並獲取初始值,預設是null。如果是帶參初始化的子類SuppliedThreadLocal,會有一個輸入初始值。
    // 當然也可以繼承ThreadLocal類重寫該方法設定初始值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    
    // 如果容器不為null,則直接設定元素。否則用執行緒物件t和value去初始化容器物件
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

移除執行緒本地變數的方法

public void remove() {
    // 如果容器不為null就呼叫容器的移除方法,移除和該threadlocal繫結的變數
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

ThreadLocal-hash值計算

ThreadLocal的hash值用於ThreadLocalMap容器計算陣列下標。類中定義threadLocalHashCode表示其hash值。類中定義了靜態方法和靜態原子變數計算hash值,也就是說所有的threadLocal物件共用一個增長器。

// 當前ThreadLocal物件的hash值
private final int threadLocalHashCode = nextHashCode();

// 用來計算hash值的原子變數,所有的threadlocal物件共用一個增長器
private static AtomicInteger nextHashCode = new AtomicInteger();

// 魔法數字,使hash雜湊均勻
private static final int HASH_INCREMENT = 0x61c88647;

// 計算hash值的靜態方法
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

我們使用同樣的方法定義一個測試類,定義多個不同測試類物件,看看hash值的生成情況。如下所示,可以看到hash值都不同,是共用的一個增長器。

public class Test{

    private static final int HASH_INCREMENT = 0x61c88647;
    public static AtomicInteger nextHashCode = new AtomicInteger();
    public final int nextHashCode = nextHashCode();
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    public static void main(String[] args){
        for (int i = 0; i < 5; i++) {
            Test test = new Test();
            System.out.println(test.nextHashCode);
        }
    }
    // 輸出的hash值
    
    0
    1640531527
    -1013904242
    626627285
    -2027808484
}

ThreadLocalMap類

ThreadLocalMap類是ThreadLocal的內部類。其作為一個容器,為ThreadLocal提供操作執行緒本地變數的功能。每一個Thread物件中都會有一個ThreadLocalMap物件例項(成員變數threadLocals,初始值為null)。因為map是Thread物件的非公共成員,不會被併發呼叫,所以不用考慮併發風險。

後文將從資料儲存設計、初始化、增刪資料等方面分析對應原始碼。

ThreadLocalMap-資料儲存設計

該map和hashmap類似,使用一個Entry陣列來儲存節點元素,定義size變數表示當前容器中元素的數量,定義threshold變數用於計算擴容的閾值。

// Entry陣列
private Entry[] table;

// 容器內元素個數
private int size = 0;

// 擴容計算用閾值
private int threshold;

不同的是Entry節點為WeakReference類的子類,使用引用欄位作為鍵,將弱引用欄位(通常是ThreadLocal物件)和值繫結在一起。使用弱引用是為了使得threadLocal物件可以被回收,(如果將key作為entry的一個成員變數,那執行緒銷燬前,threadLocal物件不會被回收掉,即使該threadLocal物件不再使用)。

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

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

ThreadLocalMap-初始化

提供了帶初始鍵和初始值的map構造方法,還有一個基於已有map的構造方法(用於ThreadLocal的子類InheritableThreadLocal初始化map容器,目的是將父執行緒的map傳入子執行緒,會在建立子執行緒的過程中自動執行)。如下所示:

// 基於初始鍵值的建構函式
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 基於輸入鍵值構建節點
    table = new Entry[INITIAL_CAPACITY];
    
    // 根據鍵的hash值計算所在陣列下標
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    
    // 採用懶載入的方式,只建立一個必要的節點
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    
    // 設定閾值為初始長度的2/3,初始長度預設為12,那麼閾值為為8
    setThreshold(INITIAL_CAPACITY);
}

// 基於已有map的建構函式
private ThreadLocalMap(ThreadLocalMap parentMap) {
    // 獲取傳入map的節點陣列
    Entry[] parentTable = parentMap.table;
    
    int len = parentTable.length;
    setThreshold(len);
    // 構造相同長度的陣列
    table = new Entry[len];
    
    // 深拷貝傳入陣列中各個節點到當前容器陣列
    // 注意這裡因為採用開放地址解決hash衝突,拷貝後的元素在陣列中的位置與原陣列不一定相同
    for (int j = 0; j < len; j++) {
    
        // 獲取陣列各個位置上的節點
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
            
                //確保key為InheritableThreadLocal型別,否則丟擲UnsupportedOperationException
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                
                // 根據hash值和陣列長度,計算下標
                int h = key.threadLocalHashCode & (len - 1);
                
                // 這裡採用開放地址的方法解決hash衝突
                // 當發生衝突時,就順延到陣列下一位,直到該位置沒有元素 
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

ThreadLocalMap-移除元素

這裡將移除元素的方法放在前面,是因為其它部分會頻繁使用過時節點的移除方法。先理解這部分內容有助於後續理解其他部分。

根據key移除容器元素的方法:

 private void remove(ThreadLocal<?> key) {
    // 計算索引下標
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    
    // 從下標i處開始向後尋找是否有key對應節點,直到遇到Null節點
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
         
        // 如果遇到key對應節點,執行移除操作
        if (e.get() == key) {
            // 移除節點的鍵(弱引用)
            e.clear();
            // 移除該過時節點
            expungeStaleEntry(i);
            return;
        }
    }
}

移除過時節點的執行方法:
移除過時節點除了將該節點置為null之外,還要對該節點之後的節點進行移動,看看能不能往前找合適的空格轉移。

這種方法有點類似jvm垃圾回收演算法的標記-整理方法。都是將垃圾清除之後,將剩餘元素進行整理,變得更緊湊。這裡的整理是需要強制執行的,目的是為了保證開放地址法一定能在連續的非null節點塊中找到已有節點。(試想,如果把過時節點移除而不整理,該節點為null,將前後節點分開了。而如果後面有某個節點hash計算的下標在前面的節點塊,在查詢節點時通過開放地址會找不到該節點)。示意圖如下:

private int expungeStaleEntry(int staleSlot) {
    // 獲取entyy陣列和長度
    Entry[] tab = table;
    int len = tab.length;

    // 清除staltSlot節點的值的引用,清除節點的引用
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 容器元素個數-1
    size--;

    // 清除staleSlot節點後的整理工作
    // 將staleSlot索引後的節點計算下標往前插空移動
    Entry e;
    int i;
    // 遍歷連續的非null節點,直到遇到null節點
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // case1:如果遍歷到的節點是過時節點,將該節點清除,容器元素數量-1
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // case2:如果遍歷到的節點不是過時節點,重新計算下標
            int h = k.threadLocalHashCode & (len - 1);
            // 當下標不是當前位置時,從hash值計算的下標h處,開放地址往後順延插空
            if (h != i) {
                // 先將該節點置為null
                tab[i] = null;

                // 找到為null的節點,插入節點
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

移除所有過時節點的方法:很簡單,全域性遍歷,移除所有過時節點。

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);
    }
}

嘗試去掃描一些過時節點並清除節點,如果有節點被清除會返回true。這裡只執行了logn次掃描判斷,是為了在不掃描和全域性掃描之間找到一種平衡,是上面的方法的一個平衡。

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;
            // 從該連續塊後第一個null節點開始
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

ThreadLocalMap-獲取元素

獲取容器元素的方法:

// 根據key快速查詢entry節點
private Entry getEntry(ThreadLocal<?> key) {
    // 通過threadLocal物件(key)的hash值計算陣列下標
    int i = key.threadLocalHashCode & (table.length - 1);
    
    // 取對應下標元素
    Entry e = table[i];
    
    if (e != null && e.get() == key)
        return e;
    else
        // 查詢不到有兩種情況:
        // 1.對應下標桶位為空
        // 2對應下標桶位元素不是key關聯的entry(開放地址解決hash衝突導致的)
        return getEntryAfterMiss(key, i, e);
}

// 初次查詢失敗後再次查詢entry節點
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    // 獲取entry陣列及長度
    Entry[] tab = table;
    int len = tab.length;
    
    // 如果e為null,說明對應下標桶位為空,找不到key對應的entry
    // 如果e不為null,則用解決hash衝突時的方法(順延陣列下一位)一直找下去,直到找到或e為null
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
            
        // 在尋找的過程中如果節點的key,即ThreadLocal已經被回收(被弱引用的物件可能會被回收)
        // 則移除過時的節點,移除過時節點的方法分析見移除元素部分
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    // 沒有找到,返回null
    return null;
}

ThreadLocalMap-增加和修改元素

增加和修改容器元素的方法:
這裡在根據hash值計算出下標後,由於是開放地址解決hash衝突,會順序向後遍歷直到遇到null或遇到key對應的節點。

這裡會出現三種情況:

case1:遍歷時找到了key對應節點,這時直接修改節點的值即可;

case2:遍歷中遇到了有過時的節點(key被回收的節點);

case3:遍歷沒有遇到過時的節點,也沒有找到key對應節點,說明此時應該插入新節點(用輸入鍵值構造新節點)。因為是增加新元素,所以可以容量會超過閾值。在刪除節點後容量如果超過閾值,則要進行擴容操作。

private void set(ThreadLocal<?> key, Object value) {
    // 獲取陣列,計算key對應的陣列下標
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    // 從下標i開始,順序遍歷陣列(順著hash衝突開放地址的路徑),直到節點為null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 獲取遍歷到的節點的key
        ThreadLocal<?> k = e.get();
        
        // case1:命中key,說明已存在key對應節點,修改value值即可
        if (k == key) {
            e.value = value;
            return;
        }
        
        // case2:如果遍歷到的節點的key為null,說明該threadLocal物件已經被回收
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    
    // case3:遍歷節點直到null都沒有找到對應key,說明map中沒有key對應entry
    // 則在該位置用輸入鍵和值新建一個entry節點
    tab[i] = new Entry(key, value);
    
    int sz = ++size;
    // 判斷是否清理過時節點後,在判斷是否需要擴容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

case2:增加和修改過程中遇到已經過時的節點的處理。這裡的引數staleSlot表示key計算的下標開始往後遇到的第一個過時節點,不管map中有無key對應的節點,該位置之後一定會存入key的節點。這裡定義了一個變數slotToExpunge,其含義是左右連續非null的entry塊中第一個過時節點(記錄該位置是為了後續清除過時節點可以從slotToExpunge處開始)。示意如下:

這步操作有兩種情況:

casse2.1:從過時節點staleSlot往後查詢遇到key對應節點,則將staleSlot處節點與key對應節點交換。然後清除整理連續塊。

casse2.2:沒遇到key對應節點,說明map中不存在key對應節點,則新建一個節點填入staleSlot處。然後清除整理連續塊。

private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
    // 獲取entry陣列和長度
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 往前移動尋找第一個過時節點(直到遇到null),如果沒找到的話說明第一個過時節點為staleslot處節點
    // slotToExpunge表示連續塊中第一個過時節點
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 從輸入下標staleSlot向後找到第一個出現的key對應的節點或過時的節點(key被回收的節點)
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        
        // case2.1:如果找到key對應的節點,則用staleSlot處節點和該節點交換,以保持hash表的順序(hash衝突時順序向後尋找)
        // 交換後的staleSlot節點及其之前的過時節點會被清除
        if (k == key) {
            // 交換staleSlot處節點和key對應節點
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            
            // 更新slotToExpunge的值,使其保持連續塊中第一個過時節點的特性,方便後續清理過時節點。
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
                
            // 從slotToExpunge開始清除整理連續塊
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

       // 如果遇到過時節點,更新slotToExpunge的值
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // case2.2:沒有找到key對應節點,增加新節點並填入staleSlot處
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    
    // 這裡如果slotToExpunge=staleSlot,說明連續塊中只有一個過時節點,且已經被新建節點填入,就不需要再整理。
    // 如果除了原staleSlot處,還有其它過時節點,從slotToExpunge開始清除整理連續塊
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

case3:增加元素後可能超過閾值導致的擴容處理

private void rehash() {
    // 清除所有過時節點
    expungeStaleEntries();

    // 在清除所有過時節點後,如果數量超過3/4的閾值,則進行擴容處理
    // setThreshold()方法非公有,threshold值一直為陣列長度的2/3,所以這裡是超過陣列長度一半就進行擴容
    if (size >= threshold - threshold / 4)
        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];
        
        // 如果為非null節點
        if (e != null) {
            ThreadLocal<?> k = e.get();
            // 如果是過時節點,則將value置為null,可以使得value的實體儘快被回收
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // 如果是正常節點,計算下標,重新填入新陣列(開放地址解決hash衝突)
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                // 新陣列元素個數+1
                count++;
            }
        }
    }
    
    // 重新設定閾值
    setThreshold(newLen);
    size = count;
    // 將變數table指向新陣列
    table = newTab;
}

ThreadLocalMap-記憶體洩露問題以及對設計的一些思考

先來聊一聊記憶體洩漏這個概念。我的理解是有一塊記憶體空間,如果不再被使用但又不能被垃圾回收器回收掉,那麼就相當於這塊記憶體少了這塊空間,即出現了記憶體洩露問題。如果記憶體洩露的空間一直在積累,那麼最終會導致可用空間一直減少,最終可能導致程式無法執行。

ThreadLocalMap中也是有可能會出現該問題的,map中entry節點的key為弱引用,如果key沒有其它強引用,是會被垃圾收集器回收的。回收之後,map中該節點的value就不會再被使用,但value又被entry節點強引用,不會被回收。這就相當於value這塊記憶體空間發生了洩露。所以能看到在原始碼中很多方法都進行了清除過時節點的操作,為的就是儘量避免記憶體洩漏。

在看原始碼時,一直在思考為什麼entry節點的鍵要採用弱引用的方式。不妨反過來思考,如果entry節點將threadLocal物件作為一個成員變數,而不是採用弱引用的方式,那麼entry節點一直對key和value保持著強引用關係,即使threadlocal物件在其它地方都不再使用,該物件也不會被回收。這就會導致entry節點永遠不會被回收(只要執行緒不終結),而且也不能主動去判斷是否切斷map中threadlocal物件的引用(不知道是否還有其它地方引用到了)。

因為map是Thread物件的一個成員變數,執行緒不終結,map是不會被回收的,如果發生了記憶體洩露的問題,可能會一直積累下去,最終導致程式發生異常。而key採用弱引用加之主動的判斷過時節點(判斷是否過時很簡單,看key是否為null即可)並進行清除處理可以最大限度的減少記憶體洩露的發生。

相關文章