ThreadLocal原理深入解析

zybing發表於2021-09-09

1. 從一次專案經歷說起

在上家公司做spark的任務排程系統時,碰到過這麼一個需求:
1.任務由一個執行緒執行,同時在執行過程中會建立多個執行緒執行子任務,子執行緒在執行子任務時又會建立子執行緒執行子任務的子任務。整個任務結構就像一棵高度為3的樹。
2.每個任務在執行過程中會生成一個任務ID,我需要把這個任務ID傳給子執行緒執行的子任務,子任務同時也會生成自己的任務ID,並把自己的任務ID向自己的子任務傳遞。
流程可由下圖所示
圖片描述

解決方案有很多,比如藉助外部儲存如資料庫,或者自己在記憶體中維護一個儲存ID的資料結構。考慮到系統健壯性和可維護性,最後採用了jdk中的InheritableThreadLocal來實現這個需求。
來看下InheritableThreadLocal的結構

public class InheritableThreadLocal extends ThreadLocal {

InheritableThreadLocal繼承自ThreadLocal,ThreadLocal可以說是一個儲存執行緒私有變數的容器(當然這個說法嚴格來說不準確,後面我們就知道為什麼),而InheritableThreadLocal正如Inheritable所暗示的那樣,它是可繼承的:使用它可使子執行緒繼承父執行緒的所有執行緒私有變數。因此我寫了個工具類,底層使用InheritableThreadLocal來儲存任務的ID,並且使該ID能夠被子執行緒繼承。

public class InheritableThreadLocalUtils {    private static final ThreadLocal local = new InheritableThreadLocal();    public static void set(Integer t) {
        local.set(t);
    }    public static Integer get() {        return local.get();
    }    public static void remove() {
        local.remove();
    }
}

可以透過這個工具類的set方法和get方法分別實現任務ID的存取。然而在Code Review的時候,有同事覺得我這程式碼寫的有問題:原因大概是InheritableThreadLocal在這裡只有一個,子執行緒的任務ID在儲存的時候會相互覆蓋掉。真的會這樣嗎?為此我們用程式碼測試下:

public static void main(String[] args) {

    ExecutorService executorService = Executors.newCachedThreadPool();    for(int i=0;i

這段程式碼開啟了10個執行緒標號從0到9,我們在每個執行緒中將對應的標號儲存到InheritableThreadLocal,然後開啟一個子執行緒,在子執行緒中獲取InheritableThreadLocal中的變數。最後的結果如下
圖片描述

每個執行緒都準確的獲取到了父執行緒對應的ID,可見並沒有覆蓋的問題。InheritableThreadLocal確實是用來儲存和獲取執行緒私有變數的,但是真實的變數並不是儲存在這個InheritableThreadLocal物件中,它只是為我們存取執行緒私有變數提供了入口而已。因為InheritableThreadLocal只是在ThreadLocal的基礎上提供了繼承功能,為了弄清這個問題我們研究下ThreadLocal的原始碼。

2. ThreadLocal原始碼解析

ThreadLocal主要方法有兩個,一個set用來儲存執行緒私有變數,一個get用來獲取執行緒私有變數。

2.1 set方法原始碼解析

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);    if (map != null)
        map.set(this, value);    else
        createMap(t, value);
}

Thread t = Thread.currentThread()獲取了當前執行緒例項t,繼續跟進第二行的getMap方法,

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */ThreadLocalMap getMap(Thread t) {    return t.threadLocals;
}

t是執行緒例項,而threadLocals明顯是t的一個成員變數,進入一探究竟

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

ThreadLocalMap是個什麼結構?

static class ThreadLocalMap {    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference> {        /** The value associated with this ThreadLocal. */
        Object value;        Entry(ThreadLocal> k, Object v) {            super(k);
            value = v;
        }
    }

ThreadLocalMap是類Thread中的一個靜態內部類,看起來像一個HashMap,但和HashMap又有些不一樣(關於它們的區別後面會講),那我們就把它當一個特殊的HashMap好了。因此set方法中第二行程式碼
ThreadLocalMap map = getMap(t)是透過執行緒例項t得到一個ThreadLocalMap。接下來的程式碼

if (map != null)
        map.set(this, value);    else
        createMap(t, value);
/**
 * Create the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param t the current thread
 * @param firstValue value for the initial entry of the map
 */void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果這個threadlocalmap為null,先建立一個threadlocalmap,然後以當前threadlocal物件為key,以要儲存的變數為值儲存到threadlocalmap中。

2.2 get方法原始碼解析

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */public T get() {
    Thread t = Thread.currentThread();
    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();
}

首先獲取當前執行緒例項t,然後透過getMap(t)方法得到threadlocalmap(ThreadLocalMap是Thread的成員變數)。若這個map不為null,則以threadlocal為key獲取執行緒私有變數,否則執行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;
}
protected T initialValue() {    return null;
}

首先獲取threadlocal的初始化值,預設為null,可以透過重寫自定義該值;如果threadlocalmap為null,先建立一個;以當前threadlocal物件為key,以初始化值為value存入map中,最後返回這個初始化值。

2.3 ThreadLocal原始碼總結

總的來說,ThreadLocal的原始碼並不複雜,但是邏輯很繞。現總結如下:

  • 1.ThreadLocal物件為每個執行緒存取私有的本地變數提供了入口,變數實際儲存線上程例項的內部一個叫ThreadLocalMap的資料結構中。

  • 2.ThreadLocalMap是一個類HashMap的資料結構,Key為ThreadLoca物件(其實是一個弱引用),Value為要儲存的變數值。

  • 3.使用ThreadLocal進行存取,其實就是以ThreadLocal物件為隱含的key對各個執行緒私有的Map進行存取。

可以用下圖的記憶體影像幫助理解和記憶
圖片描述

3. ThreadLocalMap詳解

先看原始碼

static class ThreadLocalMap {    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference> {        /** The value associated with this ThreadLocal. */
        Object value;        Entry(ThreadLocal> k, Object v) {            super(k);
            value = v;
        }
    }

3.1 ThreadLocalMap的key為弱引用

ThreadLocalMap的key並不是ThreadLocal,而是WeakReference,這是一個弱引用,說它弱是因為如果一個物件只被弱引用引用到,那麼下次垃圾收集時就會被回收掉。如果引用ThreadLocal物件的只有ThreadLocalMap的key,那麼下次垃圾收集過後該key就會變為null。

3.2 為何要用弱引用

減少了記憶體洩漏。試想我曾今儲存了一個ThreadLocal物件到ThreadLocalMap中,但後來我不需要這個物件了,只有ThreadLocalMap中的key還引用了該物件。如果這是個強引用的話,該物件將一直無法回收。因為我已經失去了其他所有該物件的外部引用,這個ThreadLocal物件將一直存在,而我卻無法訪問也無法回收它,導致記憶體洩漏。又因為ThreadLocalMap的生命週期和執行緒例項的生命週期一致,只要該執行緒一直不退出,比如執行緒池中的執行緒,那麼這種記憶體洩漏問題將會不斷積累,直到導致系統奔潰。而如果是弱引用的話,當ThreadLocal失去了所有外部強引用的話,下次垃圾收集該ThreadLocal物件將被回收,對應的ThreadLocalMap中的key將為null。下次get和set方法被執行時將會對key為null的Entry進行清理。有效的減少了記憶體洩漏的可能和影響。

3.3 如何真正避免記憶體洩漏

  • 及時呼叫ThreadLocal的remove方法

  • 及時銷燬執行緒例項

4. 總結

ThreadLocal為我們存取執行緒私有變數提供了入口,變數實際儲存線上程例項的map結構中;使用它可以讓每個執行緒擁有一份共享變數的複製,以非同步的方式解決多執行緒對資源的爭用


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/855/viewspace-2804878/,如需轉載,請註明出處,否則將追究法律責任。

相關文章