ThreadLocal源碼分析

ThreadLocal是一個線程內部的數據存儲類,可以在指定線程存儲和讀取數據,而數據對於其他線程是不可見的。日常開發中通常會比較少用到ThreadLocal,但是在一些特殊場景可以輕鬆實現一些比較複雜的需求。我們經常接觸到的Android消息機制正是使用了ThreadLocal存儲不同線程的Looper對象。

基本使用方法

我們先看看它的基本使用方法。

    private static final String TAG = "ThreadLocal";
    
    private ThreadLocal<String> mThreadLocal = new ThreadLocal<>();
    
    private void testThreadLocal() {
        mThreadLocal.set("main_thread");

        new Thread("sub_thread_1") {

            @Override
            public void run() {
                Log.d(TAG, "sub_thread_1 before: " + mThreadLocal.get());
                mThreadLocal.set("sub_thread_1");
                Log.d(TAG, "sub_thread_1 after: " + mThreadLocal.get());
            }

        }.start();

        new Thread("sub_thread_2") {

            @Override
            public void run() {
                Log.d(TAG, "sub_thread_2 before: " + mThreadLocal.get());
                mThreadLocal.set("sub_thread_2");
                Log.d(TAG, "sub_thread_2 after: " + mThreadLocal.get());
            }

        }.start();

        Log.d(TAG, "main_thread : " + mThreadLocal.get()); 
    }

我們分別在主線程給ThreadLocal設置一個初始值,然後創建兩個線程,在線程內部先嚐試讀取該ThreadLocal的值,然後在線程內部重新賦值,然後再次嘗試讀取其值。打印出來的結果如下

可以明確看到線程之間ThreadLocal存儲的值是互相獨立,當前線程無法訪問其他線程存儲的值。

源碼分析

接下來我們來分析一下內部實現原理,既然ThreadLocal是一個數據存儲類,那麼set和get方法就是它的最核心方法。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

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

從源碼可以得出,ThreadLocal的set和get方法實際是調用其靜態內部類ThreadLocalMap的set和getEntry方法,而ThreadLocalMap的實例則是通過調用getMap方法獲取到的,下面是獲取和創建ThreadLocalMap實例的源碼。

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

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

上述代碼表明ThreadLocalMap的實例是存儲在代表當前線程的Thread類對象中,而ThreadLocal的get方法中的判空操作也說明每個線程只創建並持有一個ThreadLocalMap實例。但是我們在開發中可以創建任意數量的ThreadLocal實例,說明對於每個線程來說,ThreadLocalMap和ThreadLocals是一對多的關係,那麼必然需要解決存在的衝突問題。

在分析ThreadLocalMap的set,getEntry以及如何解決衝突之前,我們先來分析一下ThreadLocalMap的結構。

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

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
		
		..........
		
}

從上訴源碼可以得知

  1. ThreadLocalMap有自己的Entry類,繼承至弱引用類,key是ThreadLocal類,成員變量value則是真正我們需要存儲的值。
  2. Entry類以數組形式存在,且數組容量初始爲16。
  3. size記錄了Entry類實際的數量,一旦超過了臨界值(臨界值通過threshold計算而來)會進行擴容(2倍)。

ThreadLocalMap雖然不是Map的實現類,但是卻實現了類似Map的功能。

ThreadLocalMap set方法源碼如下:

        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            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;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

for循環是這段代碼核心,我們首先來理解它的循環條件。

  • Entry e = tab[i];  初始值
  • e != null;循環條件,如果e == null 則終止循環
  • e = tab[i = nextIndex(i, len)]  獲取下一個Entry

循環體內部邏輯如下

  1. 取出Entry(弱引用類型)中存儲的ThreadLocal對象。
  2. 如果存儲的ThreadLocal對象和當前作爲參數傳遞過來的的key是同一對象,那麼說明命中緩存,直接更新value並返回。
  3. 如果從Entry中取出的是null, 說明該ThreadLocal已經被GC回收,替換當前的Entry對象並返回。
  4. 如果不滿足2,3條件,則循環獲取下一個Entry。

如果在循環體內並未return,根據循環終止條件,說明找到一個空位,則新建一個Entry,有效Entry數量加1。

最後的cleanSomeSlots方法主要作用是嘗試清除已經被GC回收的ThreadLocal 緩存,如果清除失敗(都沒有被GC回收)且當前有效Entry數量已經達到臨界值,則進行擴容。

set方法基本的邏輯已經分析完畢,擴容和清除部分的代碼和set中的for循環類似,不再進行深入分析。但是還遺漏一個問題,初始位置 i 是如何計算出來的,如何保證不同的ThreadLocal計算出來的i是唯一的。

            int i = key.threadLocalHashCode & (len-1);

從源碼得知,初始位置是由數組長度以及ThreadLocal的成員變量 threadLocalHashCode決定,數組長度在不進行擴容的前提下是不變的,所以i取決於每個ThreadLocal實例的threadLocalHashCode值。

    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

threadLocalHashCode是final類型的成員變量,通過調用nextHashCode()獲取,而nextHashCode()方法則是通過一個原子類AtomicInteger類型的靜態變量調用getAndAdd方法返回的(getAndAdd() 方法作用是增加值同時返回舊值)。

也就是說每次創建ThreadLocal實例,threadLocalHashCode就會增長HASH_INCREAMENT,從而保證每一個ThreadLocal擁有唯一的threadLocalHashCode,在計算初始位置的時候就避免的衝突。

ThreadLocalMap的getEntry()方法就簡單很多。

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

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

直接去找初始位置的Entry,如果匹配則返回對應value,如果不匹配,再循環查找table裏的Entry。

注意事項

在某些場景下,ThreadLocal可以極大地減小多線程需求的編碼複雜度,但是當ThreadLocal和線程池結合使用時,必須注意潛在的問題。先看以下例子。

    private static final String TAG = "ThreadLocal";

    private ThreadLocal<String> mThreadLocal = new ThreadLocal<>();

    private Executor mExecutors = Executors.newSingleThreadExecutor();

    private void testThreadLocal() {

        mExecutors.execute(new Runnable() {
            @Override
            public void run() {
                mThreadLocal.set("runnable_1");
                Log.d(TAG, "runnable_1 : " + mThreadLocal.get());
            }
        });

        mExecutors.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "runnable_2 : " + mThreadLocal.get());
            }
        });

    }

輸出log如下

可以看到儘管第二個任務並未給ThreadLocal設置值,但是卻可以直接從中取出有效值,且有效值和前一個任務設置的值一樣。

這是因爲線程池中的線程是複用的,所以會導致不同任務之間操作的實際是同一個ThreadLocal緩存,所以在線程池中如果明確ThreadLocal不再使用後需要手動remove,避免對後續任務造成干擾。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章