ThreadLocal面試六連問


轉自:碼農沉思錄

 

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

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

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

 

1. 內存泄漏原因探索

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

 

2. 怎么解決這個內存泄漏問題

  每次使用完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, 這個后面會詳細講。

 

3. 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);方法。

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

 

3. 手動釋放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一般不會處於終止狀態。

 

5. Thread和ThreadLocal有什么聯系呢

  ThreadLocal的概念。

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

 

6. 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
public static void bindResource(Object key, Object value) throws IllegalStateException {
    Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
    Assert.notNull(value, "Value must not be null");
    // ThreadLocal<Map<Obj,Obj>> resources; 從ThreadLocal中拿到value值,即Map對象
   Map
<Object, Object> map = (Map)resources.get(); if (map == null) { map = new HashMap(); resources.set(map); }
// 往Map中賦值,將DBSource與Conn分別作為kv存儲 Object oldValue
= ((Map)map).put(actualKey, value); if (oldValue instanceof ResourceHolder && ((ResourceHolder)oldValue).isVoid()) { oldValue = null; } if (oldValue != null) { throw new IllegalStateException("Already value [" + oldValue + "] for key ["
        + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); } else { if (logger.isTraceEnabled()) { logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" + Thread.currentThread().getName() + "]"); } } }

  2.事務結束階段:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion
    ->TransactionSynchronizationManager#unbindResource
    ->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource
    ->TransactionSynchronizationManager#doUnbindResource
private static Object doUnbindResource(Object actualKey) {
    // sources對象類型是ThreadLocal<Map<Obj,Obj>>
Map
<Object, Object> map = (Map)resources.get(); if (map == null) { return null; } else {
// 刪除map的Entry Object value
= map.remove(actualKey); if (map.isEmpty()) {
// 這里對ThreadLocal的Entry進行移除 resources.remove(); }
if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) { value = null; } if (value != null && logger.isTraceEnabled()) { logger.trace("Removed value [" + value + "] for key ["
         + actualKey + "] from thread [" + Thread.currentThread().getName() + "]"); } return value; } }


免責聲明!

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



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