ThreadLocal原理分析

bmilk發表於2020-05-26

接下來個人的學習方向偏向於 Android & Java 面試相關知識點系統性的總結,歡迎關注。

ThreadLocal類是java.lang包下的一個類,用於執行緒內部的資料儲存,通過它可以在指定的執行緒中儲存資料,本文針對該類進行原理分析。

通過思維導圖對其進行簡單的總結:

ThreadLocal原理分析

一.ThreadLocal原始碼分析

ThreadLocal類最重要的幾個方法如下:

  • get():T 獲取當前執行緒下儲存的變數副本
  • set(T):void 儲存該執行緒下的某個變數副本
  • remove():void 移除該執行緒下的某個變數副本

1.get()方法分析

ThreadLocal類比較簡單,其最重要的就是get()set()方法,顧名思義,起作用就是取值和設定值:

// 獲取當前執行緒中的變數副本
public T get() {
    // 獲取當前執行緒
    Thread t = Thread.currentThread();
    // 獲取執行緒中的ThreadLocalMap物件
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 獲取變數副本並返回
            T result = (T)e.value;
            return result;
        }
    }
    // 若沒有該變數副本,返回setInitialValue()
    return setInitialValue();
}
複製程式碼

這裡先將ThreadLocalMap暫時理解為一個Map結構的容器,內部儲存著該執行緒作用域下的的所有變數副本,我們從ThreadLocal類中取值的時候,實際上是從ThreadLocalMap中取值。

如果Map中沒有該變數的副本,會從setInitialValue()中取值:

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;
}
複製程式碼

可以看到,setInitialValue()中也非常的簡單,依然是從當前執行緒中獲取到ThreadLocalMap,略微不同的是,setInitialValue()會對變數進行初始化,存入ThreadLocalMap中並返回。

這個初始化的方法的執行,需要開發者自己重寫initialValue()方法,否則返回值依然為null

public class ThreadLocal<T> {
    // ...
    protected T initialValue() {
       return null;
    }
}    
複製程式碼

2.set()方法分析

setInitialValue()方法類似,set()方法也非常簡單:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // map不為空,直接將ThreadLocal物件作為key
        // 變數本身的值為value,存入map
        map.set(this, value);
    else
        // 否則,建立ThreadLocalMap
        createMap(t, value);
}
複製程式碼

可以看到,這個方法的作用就是將變數副本作為value存入Map,需要注意的是,key並非是我們下意識認為的Thread物件,而是ThreadLocal本身(ThreadValue本身是一對一的,我們更容易將其對映為key-value的關係)。

3.remove()方法分析

public void remove() {
   ThreadLocalMap m = getMap(Thread.currentThread());
   if (m != null)
       m.remove(this);
}
複製程式碼

對於變數副本的移除,也是通過map進行處理的,和set()get()相同,Entry的鍵值對中,ThreadLocal本身作為key,對變數副本進行檢索。

4.小結

可以看出,ThreadLocal本身內部的邏輯都是圍繞著ThreadLocalMap在運作,其本身更像是一個空殼,僅作為API供開發者呼叫,內部邏輯都委託給了ThreadLocalMap

接下來我們來探究一下ThreadLocalMapThread以及ThreadLocal之間的關係。

二、ThreadLocalMap分析

ThreadLocalMap內部程式碼和演算法相對複雜,個人亦是一知半解,因此就不逐行程式碼進行分析,僅系統性進行概述。

首先來看一下ThreadLocalMap的定義:

public class ThreadLocal<T> {

    // ThreadLocalMap是ThreadLocal的內部類
    static class ThreadLocalMap {

      // Entry類,內部key對應的是ThreadLocal的弱引用
      static class Entry extends WeakReference<ThreadLocal<?>> {
          // 變數的副本,強引用
          Object value;

          Entry(ThreadLocal<?> k, Object v) {
              super(k);
              value = v;
          }
      }
    }
}
複製程式碼

ThreadLocal中的巢狀內部類ThreadLocalMap本質上是一個map,依然是key-value的形式,其中有一個內部類Entry,其中key可以看做是ThreadLocal例項的弱引用。

和最初的設想不同的是,ThreadLocalMapkey並非是執行緒的例項Thread,而是ThreadLocal,那麼ThreadLocalMap是如何保證同一個Thread中,ThreadLocal的指定變數唯一呢?

// 1.ThreadLocal的set()方法
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // ...
}

// 2.getMap()實際上是從Thread中獲取threadLocals成員
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

public class Thread implements Runnable {
    // 3.每個Thread例項都持有一個ThreadLocalMap的屬性
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
複製程式碼

Thread本身持有ThreadLocal.ThreadLocalMap的屬性,每個執行緒在向ThreadLocalsetValue的時候,其實都是向自己的ThreadLocalMap成員中加入資料;get()同理。

三、記憶體洩漏的風險?

在上一小節中,我們看到ThreadLocalMap中的Entry中,其ThreadLocal作為key,是作為弱引用進行儲存的。

ThreadLocal不再被作為強引用持有時,會被GC回收,這時ThreadLocalMap對應的ThreadLocal就變成了null。而根據文件所敘述的,當key == null時,這時就可以預設該鍵不再被引用,該Entry就可以被直接清除,該清除行為會在Entry本身的set()/get()/remove()中被呼叫,這樣就能 一定情況下避免記憶體洩漏

這時就有一個問題出現了,作為keyThreadLocal變成了null,那麼作為value的變數可是強引用呀,這不就導致記憶體洩漏了嗎?

其實一般情況下也不會,因為即使再不濟,執行緒在執行結束時,自然也會消除其對value的引用,使得Value能夠被GC回收。

當然,在某種情況下(比如使用了 執行緒池),執行緒再次被使用,Value這時依然可以被獲取到,自然也就發生了記憶體洩漏,因此此時,我們還是需要通過手動將value的值設定為null(即呼叫ThreadLocal.remove()方法)以規避記憶體洩漏的風險。

參考&感謝


關於我

Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的部落格或者Github

如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章