天天看點

【06期】面試官:說一下ThreadLocal和記憶體洩漏的問題

什麼是ThreadLocal

ThreadLocal是Java中的一個類,它提供了一種将對象與線程關聯的機制。每個線程都可以通過ThreadLocal擷取自己的獨立副本,以保證線程間的資料隔離和線程安全。

ThreadLocal的作用主要有以下幾個方面:

  1. 線程隔離:每個線程可以獨立地操作自己的副本,互不幹擾。
  2. 線程封閉:通過ThreadLocal,可以将可變的資料封裝在每個線程内部,使其具有線程級别的封閉性。
  3. 上下文傳遞:ThreadLocal常被用于存儲線程相關的上下文資訊,如使用者身份、請求參數等。
  4. 避免傳參:有些情況下,某些資料需要在多個方法之間傳遞,使用ThreadLocal可以避免頻繁的參數傳遞。

ThreadLocal是如何工作的?

ThreadLocal核心邏輯都是在Thread類的靜态内部類ThreadLocalMap中,接下來一起分析下ThreadLocalMap源碼,逐個方法解析:

ThreadLocalMap類中有個靜态内部類Entry,Entry可以了解為存儲資料的地方。源碼如下:

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

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

通過上邊源碼發現Entry竟然繼承WeakReference,而且ThreadLocal竟然是個弱引用。

弱引用:弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的記憶體區域的過程中,一旦發現了「隻具有弱引用」的對象,不管目前記憶體空間足夠與否,都會回收它的記憶體

也就是說ThreadLocal随時可能消失,也就是entry.get()傳回的ThreadLocal随時可能為空。

//這個線程的關于所有的ThreadLocal儲存的資料都在這,通過ThreadLocal确定下坐标找到對應的entry
private Entry[] table;
private void set(ThreadLocal<?> key, Object value) {
  
        Entry[] tab = table;
        int len = tab.length;
     //通過threadLocal确定下坐标找到對應的entry
        int i = key.threadLocalHashCode & (len-1);

    //周遊數組
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
   
            if (k == key) {
                e.value = value;
                return;
            }
   //這裡!!!如果e不是null,但是e.get()是null,表示那個弱引用被垃圾回收了
            if (k == null) {
                //這個方法會删除所有過期的條目
                //意思就是将被垃圾回收掉的threadLocal,從資料table中删除
                //并且新new 一個entry代替掉它的i坐标,過分!
                replaceStaleEntry(key, value, i);
                return;
            }
        }
  //如果沒有直接新增一個到數組裡邊
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
    }
           

通過上邊的set方法,可以發現ThreadLocalMap真正存儲資料的地方是table數組裡邊,通過threadLocal的hash值找下坐标。如果在set的時候發現table中threadLocal被回收掉了,那麼它的位置直接被頂替掉。

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

這個get方法沒什麼好說的,就是用threadLocal的hash值去找table的下坐标,沒有找到直接循環數組找。好了ThreadLocalMap的核心邏輯就分析到這。

接下來看ThreadLocal核心源碼

public void set(T value) {
    // 拿到目前線程
 Thread t = Thread.currentThread();
    //擷取目前線程下的ThreadLocalMap
 ThreadLocalMap map = getMap(t);
    
 if (map != null)
  map.set(this, value);
 else
  createMap(t, value);
}

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

public T get() {
    // 拿到目前線程
 Thread t = Thread.currentThread();
     //擷取目前線程下的ThreadLocalMap
 ThreadLocalMap map = getMap(t);
 if (map != null) {
        //通過threadLocal擷取table裡邊的value值
  ThreadLocalMap.Entry e = map.getEntry(this);
  if (e != null) {
   @SuppressWarnings("unchecked")
   T result = (T) e.value;
   return result;
  }
 }
 return setInitialValue();
}
           
【06期】面試官:說一下ThreadLocal和記憶體洩漏的問題

threadLocal和ThreadMap的關系

圖解釋:

建了三個ThreadLocal,有兩個線程,線上程裡邊ThreadLocalMap類的屬性table分别儲存着threadLocal對應的value值。

ThreadLocal記憶體洩漏問題

記憶體洩漏:是指程式在申請記憶體後,無法釋放已申請的記憶體空間,導緻系統無法及時回收記憶體并且配置設定給其他程序使用。通常少次數的記憶體無法及時回收并不會到程式造成什麼影響,但是如果在記憶體本身就比較少擷取多次導緻記憶體無法正常回收時,就會導緻記憶體不夠用,最終導緻記憶體溢出。

首先「說一下為什麼在ThreadLocalMap中ThreadLocal是弱引用?」

如果在ThreadLocalMap中使用強引用來存儲ThreadLocal對象,那麼ThreadLocal對象會一直存在于記憶體中,即使在實際的應用中已經不再需要該ThreadLocal對象。這是因為ThreadLocalMap是與線程相關聯的,儲存在ThreadLocalMap的table數組中,ThreadLocalMap中的鍵值對不會被自動清理,而是會一直保留,進而造成記憶體洩漏。也就是說ThreadLocal永遠不會被清理,除非手動清理,ThreadLocalMap調用set(),get(),remove()方法的時候會被清除value值。

為了解決這個問題,ThreadLocal被存儲為弱引用。弱引用的特點是,當一個對象隻被弱引用所引用時,在垃圾回收時會被自動回收。當ThreadLocal對象被垃圾回收時,對應的鍵值對也會被自動從ThreadLocalMap中移除。

「threadLocal記憶體洩漏問題」

如果一個線程長時間運作,一直持有ThreadLocal對象的鍵值,由于value被強制引用,沒有及時清理導緻記憶體洩漏。

為了避免ThreadLocal記憶體洩漏問題,需要進行适當的清理操作。可以使用ThreadLocal類提供的remove()方法手動清理ThreadLocal對象關聯的值。

繼續閱讀