ThreadLocal原理深入解析



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

解決方案有很多,比如借助外部存儲如數據庫,或者自己在內存中維護一個存儲ID的數據結構。考慮到系統健壯性和可維護性,最后采用了jdk中的InheritableThreadLocal來實現這個需求。
來看下InheritableThreadLocal的結構

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

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

public class InheritableThreadLocalUtils {

    private static final ThreadLocal<Integer> 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;i++){
        executorService.execute(new TaskThread(i));
    }

}

static class TaskThread implements Runnable{

    Integer taskId;

    public TaskThread(Integer taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        InheritableThreadLocalUtils.set(taskId);
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(InheritableThreadLocalUtils.get());
            }
        });
    }
}

這段代碼開啟了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<ThreadLocal<?>> {
        /** 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<ThreadLocal<?>> {
        /** 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>,這是一個弱引用,說它弱是因為如果一個對象只被弱引用引用到,那么下次垃圾收集時就會被回收掉。如果引用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結構中;使用它可以讓每個線程擁有一份共享變量的拷貝,以非同步的方式解決多線程對資源的爭用


免責聲明!

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



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