天天看點

ThreadLocal底層原理是什麼?什麼是ThreadLocal使用場景API原理資料結構記憶體洩漏問題總結

文章已收錄Github精選,歡迎Star: https://github.com/yehongzhi/learningSummary

什麼是ThreadLocal

ThreadLocal提供線程的局部變量,這種變量與普通變量的差別在于,每個通路這種變量的線程都有自己的、獨立的變量副本。用于解決多線程間的資料隔離問題。

使用場景

其實ThreadLocal在很多開源架構中都有應用:

  • Spring中的事務管理器,比如TransactionSynchronizationManager等。
  • Mybatis中的ErrorContext類,使用ThreadLocal實作線程安全的單例。
  • 存儲session中的一些參數,比如使用者資訊等。

API

ThreadLocal提供了4個常用方法:

  • set()

    方法,設定目前線程中變量的副本。
  • get()

    方法,擷取

    ThreadLocal

    在目前線程中儲存的變量副本。
  • remove()

    方法,清空目前線程中變量的副本。
  • initialValue()

    是一個

    protected

    方法,一般是用來重寫的,如果在沒有set的時候就調用

    get

    ,會調用

    initialValue

    方法初始化内容。
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
    //重寫此方法,初始化ThreadLocal的value
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};           

原理

那麼怎麼實作資料隔離的,我們從源碼的角度進行分析。

我們先看ThreadLocal類的get()方法。

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;
        }
    }
    return 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;
}
//傳回Thread執行個體的成員變量threadLocals
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//給Thread執行個體的成員變量threadLocals指派
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}           

從源碼可以看出,資料是存在于Thread類的成員變量threadLocals

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;           

上面寫了一段注釋,翻譯過來就是,關于該線程的ThreadLocal的值,由ThreadLocal類進行維護。

是以很清楚了,資料隔離的實作是因為ThreadLocal類操作的是Thread的成員變量threadLocals。每個線程Thread都有自己的threadLocals,進而互相不影響。

threadLocals這個成員變量的本質又是ThreadLocalMap類,它是ThreadLocal的内部類,下面我們研究一下這個内部類的資料結構。

資料結構

先看一下源碼:

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

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //初始化容量
    private static final int INITIAL_CAPACITY = 16;
    //散清單
    private Entry[] table;
    //有效數量
    private int size = 0;
    //負載因子
    private int threshold;
    
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
    //構造器
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
}           

這一看跟HashMap還有幾分相似,但是哈希沖突的處理方式,ThreadLocalMap采用的是開放尋址法(自行百度一下,這裡不多解釋了),大概長這個樣子:

ThreadLocal底層原理是什麼?什麼是ThreadLocal使用場景API原理資料結構記憶體洩漏問題總結

是以這裡可以看出ThreadLocal的引用可以定位到ThreadLocalMap裡散清單table[]裡的值。

記憶體洩漏問題

ThreadLocal底層原理是什麼?什麼是ThreadLocal使用場景API原理資料結構記憶體洩漏問題總結

我們從源碼中可以看到Entry是繼承WeakReference類,key是弱引用,value是強引用。為什麼要設計成弱引用?不如反過來想,如果設定成強引用會有什麼效果。

如果Entry對象的Key每個都強引用到ThreadLocal對象的話,那麼這個ThreadLocal對象就會因為和Entry對象存在強引用關聯而無法被GC回收,造成記憶體洩漏,除非線程結束後,線程被回收了,ThreadLocalMap才會跟着回收。

當作為Key的ThreadLocal對象設定成弱引用對象後,在系統GC的時候,ThreadLocal對象就會被回收。

但是這樣就能防止記憶體洩漏嗎?

其實不然!因為Value還是強引用對象,當Key被回收後,key變成了null值,而Value依然存在一條強引用鍊:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永遠無法回收,而這塊value也永遠不會被通路到了,最終造成記憶體洩漏。

是以在設計ThreadLocalMap時就考慮到這個問題,在ThreadLocal的

get()、set()、remove()

的時候都會清除線程ThreadLocalMap裡所有key為

null

的value。

總結

  • 其實ThreadLocal并沒有解決多線程間資料共享的問題,而是使資料在不同線程有不同的副本,那麼就不需要解決共享資料的問題。
  • 每個線程持有一個ThreadLocalMap對象,該ThreadLocalMap對象隻會被持有它的線程通路,是以不存線上程安全問題。
  • ThreadLocalMap的資料結構類似HashMap,裡面由Entry[]數組、size、負載因子等組成,采用開放尋址法解決哈希沖突。
  • ThreadLocalMap的Entry對ThreadLocal對象是弱引用,GC回收後,會産生一些key為null的value無法被通路,也無法被回收,最終導緻記憶體洩漏。預防措施是調用ThreadLocal的remove()方法,清除掉ThreadLocalMap裡面key為null的value。

非常感謝你的閱讀,希望這篇文章能給到你幫助和啟發。

覺得有用就點個贊吧,你的點贊是我創作的最大動力~

我是一個努力讓大家記住的程式員。我們下期再見!!!

能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!