ThreadLocal內存泄漏簡談

ThreadLocal

內存泄漏

ThreadLocal內存泄露,最主要的原因在於它的內部類ThreadLocalMap中的Entry的設計。Entry繼承了WeakReference<ThreadLocal<?>>,即Entry的key是弱引用,所以key'會在垃圾回收的時候被回收掉, 而key對應的value則不會被回收, 這樣會導致一種現象:key爲null,value有值。key爲空的話value是無效數據,久而久之,value累加就會導致內存泄漏。

static class ThreadLocalMap {
       static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    ...
}
怎麼解決這個內存泄漏問題

每次使用完ThreadLocal都調用它的remove()方法清除數據。

JDK開發者是如何避免內存泄漏的

ThreadLocal提供的get()方法中,調用了ThreadLocalMap#getEntry()方法,對key進行了校驗和對null key進行擦除。

        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
                return getEntryAfterMiss(key, i, e);
        }

如果key爲null, 則會調用getEntryAfterMiss()方法,在這個方法中,如果k == null , 則調用expungeStaleEntry(i);方法。

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

expungeStaleEntry(i)方法完成了對key=null 的key所對應的value進行賦空, 釋放了空間避免內存泄漏。

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

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            // 遍歷下一個key爲空的entry, 並將value指向null
            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;

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

同理, set()方法最終也是調用該方法(expungeStaleEntry), 調用路徑: set(T value)->map.set(this, value)->rehash()->expungeStaleEntries()

remove方法remove()->ThreadLocalMap.remove(this)->expungeStaleEntry(i)

這樣做, 也只能說盡可能避免內存泄漏, 但並不會完全解決內存泄漏這個問題。比如極端情況下我們只創建ThreadLocal但不調用set、get、remove方法等。所以最能解決問題的辦法就是用完ThreadLocal後手動調用remove().

手動remove?你怎麼去設計/實現?

如果是spring項目, 可以藉助於bean的聲明週期, 在攔截器的afterCompletion階段進行調用。

弱引用導致內存泄漏,那爲什麼key不設置爲強引用

如果key設置爲強引用, 當threadLocal實例釋放後, threadLocal=null, 但是threadLocal會有強引用指向threadLocalMap,threadLocalMap.Entry又強引用threadLocal, 這樣會導致threadLocal不能正常被GC回收。

弱引用雖然會引起內存泄漏, 但是也有set、get、remove方法操作對null key進行擦除的補救措施, 方案上略勝一籌。

線程執行結束後會不會清空value

事實上,當currentThread執行結束後, threadLocalMap變得不可達從而被回收,Entry等也就都被回收了,但這個環境就要求不對Thread進行復用,但是我們項目中經常會複用線程來提高性能, 所以currentThread一般不會處於終止狀態。

Thread和ThreadLocal有什麼聯繫呢

Thread和ThreadLocal是綁定的, ThreadLocal依賴於Thread去執行, Thread將需要隔離的數據存放到ThreadLocal(準確的講是ThreadLocalMap)中, 來實現多線程處理。

相關問題擴展

spring如何處理bean多線程下的併發問題

ThreadLocal天生爲解決相同變量的訪問衝突問題, 所以這個對於spring的默認單例bean的多線程訪問是一個完美的解決方案。spring也確實是用了ThreadLocal來處理多線程下相同變量併發的線程安全問題。

spring 如何保證數據庫事務在同一個連接下執行的

要想實現jdbc事務, 就必須是在同一個連接對象中操作, 多個連接下事務就會不可控, 需要藉助分佈式事務完成。那spring 如何保證數據庫事務在同一個連接下執行的呢?

DataSourceTransactionManager 是spring的數據源事務管理器, 它會在你調用getConnection()的時候從數據庫連接池中獲取一個connection, 然後將其與ThreadLocal綁定, 事務完成後解除綁定。 這樣就保證了事務在同一連接下完成。

詳細源碼:

  1. 事務開始階段:org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin->TransactionSynchronizationManager#bindResource->org.springframework.transaction.support.TransactionSynchronizationManager#bindResource
  1. 事務結束階段:

    org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion->TransactionSynchronizationManager#unbindResource->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource->TransactionSynchronizationManager#doUnbindResource



如果你喜歡我的文章,那麻煩請關注我的公衆號,該公衆號還處於初始階段,謝謝大家的支持。


關注公衆號,回覆java架構獲取架構視頻資源(後期還會分享不同的優質資源噢)。回覆找對象可以拉你進IT單身交友羣噢。

想看往期文章, 請點擊我的GitHub地址: https://github.com/fantj2016/java-reader



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