來探討一下最近面試問的ThreadLocal問題


中高級階段開發者出去面試,應該躲不開ThreadLocal相關問題,本文就常見問題做出一些解答,歡迎留言探討。

ThreadLocal為java並發提供了一個新的思路, 它用來存儲Thread的局部變量, 從而達到各個Thread之間的隔離運行。它被廣泛應用於框架之間的用戶資源隔離、事務隔離等。

 

但是用不好會導致內存泄漏, 本文重點用於對它的使用過程的疑難解答, 相信仔細閱讀完后的朋友可以隨心所欲的安全使用它。

內存泄漏原因探索

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()方法清除數據。因為它的remove方法會主動將當前的key和value(Entry)進行清除。

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(); // 清除key
            expungeStaleEntry(i);  // 清除value
            return;
        }
    }
}

 

e.clear()用於清除Entry的key,它調用的是WeakReference中的方法:this.referent = null

expungeStaleEntry(i)用於清除Entry對應的value, 這個后面會詳細講。

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

ThreadLocal的設計者也意識到了這一點(內存泄漏), 他們在一些方法中埋了對key=null的value擦除操作。

這里拿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進行賦空, 釋放了空間避免內存泄漏。

同時它遍歷下一個key為空的entry, 並將value賦值為null, 等待下次GC釋放掉其空間。

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().

手動釋放ThreadLocal遺留存儲?你怎么去設計/實現?

這里主要是強化一下手動remove的思想和必要性,設計思想與連接池類似。

包裝其父類remove方法為靜態方法,如果是spring項目, 可以借助於bean的聲明周期, 在攔截器的afterCompletion階段進行調用。

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

這個問題就比較有深度了,是你談薪的小小資本。

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

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

線程執行結束后會不會自動清空Entry的value

一並考察了你的gc基礎。

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

Thread和ThreadLocal有什么聯系呢

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

來探討一下最近面試問的ThreadLocal問題

2.事務結束階段:

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

來探討一下最近面試問的ThreadLocal問題

 

Java知音10月基礎篇

聊聊 HashMap 和 TreeMap 的內部結構

感受lambda之美,推薦收藏,需要時查閱

多線程基礎體系知識清單

了解Java反射機制?看這篇就行!

從實踐角度重新理解BIO和NIO

java Integer包裝類裝箱的一個細節


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM