最近學習了“ThreadLocal”

平常沒引起注意,被“貝殼找房”的面試提到“ThreadLocal解決內存泄漏?”後來發現其實我們最常用分頁pageHelper就在使用;這也反映了平常對代碼的深入研究程度,開此篇“最近學習了。。。”

ThreadLocalMap內部Entry中key使用的是對ThreadLocal對象的弱引用(key繼承weekReference),這爲避免內存泄露是一個進步,因爲如果是強引用,那麼即使其他地方沒有對ThreadLocal對象的引用,ThreadLocalMap中的ThreadLocal對象還是不會被回收,而如果是弱引用則這時候ThreadLocal引用是會被回收掉的,雖然對於的value還是不能被回收,這時候ThreadLocalMap裏面就會存在key爲null但是value不爲null的entry項,雖然ThreadLocalMap提供了set,get,remove方法在一些時機下會對這些Entry項進行清理,但是這是不及時的,也不是每次都會執行的,所以一些情況下還是會發生內存泄露,所以在使用完畢後即使調用remove方法纔是解決內存泄露的王道。
————————————————
版權聲明:本文爲CSDN博主「加多」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/zhailuxu/article/details/79067467

8.2.2 線程池中使用ThreadLocal導致的內存泄露:

總結:線程池裏面設置了ThreadLocal變量一定要記得及時清理,因爲線程池裏面的核心線程是一直存在的,如果不清理,那麼線程池的核心線程的threadLocals變量一直會持有ThreadLocal變量。 

Java提供的ThreadLocal給我們編程提供了方便,但是如果使用不當也會給我們帶來致命的災難,編碼時候要養成良好的習慣,線程中使用完ThreadLocal變量後,要記得及時remove掉。

評論是戰區

一.ThreadLoacl的理解:

官方的講:

ThreadLocal是一個本地線程副本變量工具類,主要用於將私有線程和該線程存放的副本對象做一個映射,各個線程之間的變量互不干擾

通俗的講:

ThreadLocal也叫做線程本地變量,ThreadLoacl爲變量在每個線程中的都創建了副本,每個線程可以訪問自己內部的副本變量,線程之間互不影響

 

二.TreadLocal的原理:

從上圖我們可以初步窺見ThreadLocal的核心機制:

1)每個Thread線程內部都有一個Map

2)Map裏面儲存線程本地對象key和線程的變量副本value

3)Thread內部的Map是由ThreadLocal維護的,由ThreadLocal負責向Map獲取和設置線程的變量值

這樣對於不同的線程,每次獲取副本值時,別的線程並不能獲取到當前線程的副本值,這樣就形成了副本隔離,互不干擾

 

三.ThreadLocal的底層源碼

ThreadLocal類提供了以下幾個核心方法:

1.get方法:獲取當前線程的副本變量值

2.set方法:設置當前線程的副本變量值

3.remove方法:移除當前線程的副本變量值

4.initilaValue方法:初始化當前線程的副本變量值,初始化null

1)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)
            return (T)e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

複製代碼

源碼解析:

1.獲取當前線程的ThreadLocalMap對象threadLocals(實際儲存副本值的Map)

2.Map不爲空的話,從Map中獲取線程儲存的K-V Entry結點,然後從Entry結點中獲取Value副本值返回

3.Map爲空的話,返回初始值null,之後還需向Map中添加value爲null的鍵值對,避免空指針異常

 

2.ThreadLocal.set():

複製代碼

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

複製代碼

源碼解析:

1.獲取當前線程的成員變量Map

2.Map不爲空:重新將ThreadLocal對象和Value副本放入Map中

3.Map爲空:對線程成員變量ThreadLocalMap進行初始化創建,並將ThreadLocal對象和Value副本放入Map中

 

3.ThreadLocal.remove():

複製代碼

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

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

複製代碼

源碼分析:

直接調用了ThreadLocalMap的remove方法(後面我們還會探究ThreadLocalMap類的底層源碼!,這裏先放着)

 

4.ThreadLocal.initialValue() :

protected T initialValue() {
    return null;
}

就是直接返回null

 

小結一下:我們發現ThreadLocal的底層源碼都有一個ThreadLocalMap類,那麼ThreadLocalMap類的底層源碼又是什麼樣子的呢?我們一起來看看吧!

 

四.ThreadLocalMap的底層源碼分析

ThreadLocalMap是ThreadLocal內部的一個Map實現,然而它沒有實現任何集合的接口規範,因爲它僅供ThreadLocal內部使用,數據結構採用數組+開方地址法,Entry繼承WeakRefrence,是基於ThreadLocal這種特殊場景實現的Map,它的實現方式很值得我們取研究!!

 

1.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;
    }
}

複製代碼

源碼分析:

1.Entry中key只能是ThreadLocal對象,被規定死了的

2.Entry繼承了WeakRefrence(弱引用,生存週期只能活到下次GC前),但是隻有Key是弱引用,Value並不是弱引用

ps:value既然不是弱引用,那麼key在被回收之後(key=null)Value並沒有被回收,如果當前線程被回收了那還好,這樣value也和線程一起被回收了,要是當前線程是線程池這樣的環境,線程結束沒有銷燬回收,那麼Value永遠不會被回收,當存在大量這樣的value的時候,就會產生內存泄漏,那麼Java 8中如何解決這個問題的呢?

解決辦法:

這裏寫圖片描述

以上是ThreadLocalMap的set方法,for循環遍歷整個Entry數組,遇到key=null的就會替換,這樣就不存在value內存泄漏的問題了!!!

 

2.ThreaLocalMap中key的HashCode計算

ThreaLocalMap的key是ThreaLocal,它不會傳統的調用ThreadLocal的hashcode方法(繼承自object的hashcode),而是調用nexthashcode,源碼如下:

 

private final int threadLocalHashCode = nextHashCode();

 private static AtomicInteger nextHashCode = new AtomicInteger();

 //1640531527 這是一個神奇的數字,能夠讓hash槽位分佈相當均勻
 private static final int HASH_INCREMENT = 0x61c88647; 

 private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
 }

 

源碼分析:

我們發現ThreaLocalMap的hashcode計算沒有采用模長度的方法,沒有采用拉鍊法,採用的是開放地址法,其槽位採用靜態的AtomicInteger每次增加1640531527實現,衝突了則加1或者減1繼續進行增加1640531527

我們把這個數叫做魔數,通過這個魔數我們可以位key產生完美的槽位分配,hahs衝突的次數很少

(據說魔數和黃金比例,斐波那契數列存在某種關係)

 

3.ThreaLocalMap中set方法:

 

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

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 用key的hashCode計算槽位
    // hash衝突時,使用開放地址法
    // 因爲獨特和hash算法,導致hash衝突很少,一般不會走進這個for循環
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // key 相同,則覆蓋value
            e.value = value; 
            return;
        }

        if (k == null) { // key = null,說明 key 已經被回收了,進入替換方法
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 新增 Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些過期的值,並判斷是否需要擴容
        rehash(); // 擴容
}

 

源碼分析:

1.先是計算槽位

2.Entry數組中存在需要插入的key,直接替換即可,存在key=null,也是替換(可以避免value內存泄漏)

3.Entry數組中不存在需要插入的key,也沒有key=null,新增一個Entry,然後判斷一下需不需要擴容和清除過期的值(關於擴容和清除過期值先不細講)

 

4.ThreadLocalMap中getEntry方法:

 

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) // 無hash衝突情況
        return e;
    else
        return getEntryAfterMiss(key, i, e); // 有hash衝突情況
}

源碼分析:

1.計算槽位i,判斷table[i]是否有目標key,沒有(hahs衝突了)則進入getEntryAfterMiss方法

getEntryAfterMiss方法分析:

 

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); // 清除過期的slot
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

源碼分析:

遇到hash衝突之後繼續向後查找,並且會在查找路上清除過期的slot

 

5.ThreadLocalMap中rehash方法:

 

private void rehash() {
    expungeStaleEntries();

   // 清除過程中,size會減小,在此處重新計算是否需要擴容
   // 並沒有直接使用threshold,而是用較低的threshold (約 threshold 的 3/4)提前觸發resize
    if (size >= threshold - threshold / 4)
        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);
    }
}

 

源碼分析:

先調用expungeStaleEntries()清除所有過期的slot,然後提前觸發resize(約 threshold 的 3/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];
        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;
}

 

擴容2倍,同時在Entry移動過程中會清除一些過期的entry

 

6.ThreadLocal中的remove方法:

 

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

 

源碼分析:

遍歷Entry數組尋找需要刪除的ThreadLocal,建議在ThreadLocal使用完成之後再調用此方法

 

現在再詳細分析一下ThreadLocalMap的set方法中的幾個方法:

1.replaceStaleEntry方法:替換

 

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 往前尋找過期的slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 找到 key 或者 直到 遇到null 的slot 才終止循環
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 如果找到了key,那麼需要將它與過期的 slot 交換來維護哈希表的順序。
        // 然後可以將新過期的 slot 或其上面遇到的任何其他過期的 slot 
        // 給 expungeStaleEntry 以清除或 rehash 這個 run 中的所有其他entries。

        if (k == key) {
            e.value = value;

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

            // 如果存在,則開始清除前面過期的entry
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果我們沒有在向前掃描中找到過期的條目,
        // 那麼在掃描 key 時看到的第一個過期 entry 是仍然存在於 run 中的條目。
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 如果沒有找到 key,那麼在 slot 中創建新entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果還有其他過期的entries存在 run 中,則清除他們
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

 

上文中run的意思不好翻譯,理解爲開放地址中一個slot中前後不爲null的連續entry

 

2.cleanSomeSlots方法:清除一些slot(按照規則清除“一些”slot,而不是全部)

 

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); // 清除方法 
        }
    } while ( (n >>>= 1) != 0);  // n = n / 2, 對數控制循環 
    return removed;
}

 

當新元素被添加時,或者另外一個過期元素已經被刪除的時候,會調用該方法,該方法會試探性的掃描一些Entry尋找過期的條目,它執行對數數量的掃描,是一種基於不掃描(快速但保留垃圾)和所有元素掃描之間的平衡!!

對數數量的掃描!!!

這是一種折中的方案

 

3.expungeStaleEntry:清除

 

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

    // 清除當前過期的slot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash 直到 null 的 slot
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

 

真正的清除,不僅會清除當前過期的slot,還會繼續往後查詢直到遇到null的slot爲止,對於查詢遍歷中沒有被回收的情況,做了一次rehash

 

推薦大佬的博客:https://www.cnblogs.com/micrari/p/6790229.html

寫的太詳細了,太強了,源碼註釋賊多

---------------------------------------------------------------------------------------------

沒有下文了,這是底線…

 

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