ThreadLocal與ThreadLocalMap原始碼解析

許佳佳233發表於2017-07-26

文章個人見解較多,如有問題歡迎指出。
#使用場景
一般來說,某些資料是以執行緒為作用域並且不同執行緒具有不同的資料的資料副本的時候,就可以考慮採用ThreadLocal。
比如Looper 內部就是通過ThreadLocal來保證不同執行緒中有不同的唯一副本的,從而實現了我們現在經常使用的android訊息機制。

對於android訊息機制不瞭解,並且好奇的同學可以先看一下筆者的博文:
Handler原始碼解析

#文章摘要
1、ThreadLocal內部就是通過操作ThreadLocalMap從而實現的ThreadLocal的儲存。因此ThreadLocalMap可以稱之為ThreadLocal的儲存結構。ThreadLocalMap是ThreadLocal的內部類。
2、ThreadLocalMap內部是建立的Entry陣列進行儲存,Entry是一個弱引用,表明只要垃圾回收執行時,會回收掉它儲存的物件。(至於為什麼使用弱引用後文有闡述)
3、Thread中的threadLocals這個變數用於繫結一個ThreadLocalMap,而執行緒中所有建立的ThreadLocal 都會儲存在這個threadLocals中。這樣就保證了不同執行緒中的副本不會互相影響,因為ThreadLocalMap,即ThreadLocal的儲存容器是與執行緒本身繫結的。
4、ThreadLocalMap如果發生衝突,會讓index++,到下一個位置存取值,如果下一個位置還產生衝突,那麼就繼續index++。只有三種情況會跳出迴圈:

  1. 該位置的Entry為空,那麼就用key和value新建一個Entry賦值到這個位置 。
  2. 該位置的ThreadLocal為null,替換這個廢棄的Entry。(一般情況就是由於弱引用被垃圾回收機制回收了)
  3. 該位置的ThreadLocal等於我們要賦值的ThreadLocal,直接使用value覆蓋那個值。

#ThreadLocal初始化

    public ThreadLocal() {
    }
    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();
        
    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

可以看到ThreadLocal預設的建構函式是空的,但是在ThreadLocal中卻有初始化的內容。
threadLocalHashCode 顧名思義是一個用來保證ThreadLocal物件唯一性的一個int,我們一般稱為hash,它僅僅會在ThreadLocal的內部類ThreadLocalMap中使用。
#ThreadLocalMap

筆者建議,要了解ThreadLocal的原始碼首先要了解HashMap原始碼,否則可能對ThreadLocalMap難以理解。

它是ThreadLocal的內部靜態類,用於儲存執行緒中ThreadLocal的資料結構。ThreadLocal內部就是通過操作ThreadLocalMap從而實現的ThreadLocal的儲存。
##ThreadLocalMap的初始化

        private static final int INITIAL_CAPACITY = 16;
        
        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);
        }
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

它首先建立一個大小為16的實體陣列,然後將threadLocalHashCode 和(length-1)進行與操作從而求得index,這個操作與threadLocalHashCode %length相等,之所以用與操作是因為更快。
獲取到index之後,就把這第一個值存入陣列。
至於設定的threshold ,其實就是代表著可用的length,如果超過了這個值,就會執行擴容操作。

對於“&”操作和threshold ,閱讀過HashMap原始碼的同學肯定深知其中原理,如果不解並且好奇者,可以看一下筆者的部落格:
HashMap原始碼分析
##ThreadLocalMap.Entry()

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

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

這是ThreadLocalMap的內部類,ThreadLocalMap內部就是建立Entry 來儲存的ThreadLocal。
這裡要注意Entry 繼承的弱引用,即只要垃圾回收執行,這個物件便會被回收。

###為什麼要使用弱引用呢?
筆者個人見解,在使用執行緒池的情況下,核心執行緒是不斷複用不會被回收的,而ThreadLocalMap是與執行緒繫結的,因此會一直保留對ThreadLocalMap的引用。(即垃圾回收無法回收掉ThreadLocalMap)
如果不使用弱引用,那麼線上程替換了一個Runnable之後,之前的Runnable用ThreadLocal儲存的物件還會存在,並且不會被回收,這就發生了真正意義上的記憶體洩露,並且隨著執行緒的複用次數的提高,ThreadLocalMap中的物件一直不會被回收,很可能會造成OOM。
##ThreadLocalMap.set()

       private void set(ThreadLocal key, Object value) {

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

這裡有一個迴圈,index會一直執行加1操作(但是小於length),有三種情況會停止:

  1. 該位置的Entry為空,那麼就用key和value新建一個Entry賦值到這個位置
  2. 到該位置的ThreadLocal為null,替換這個廢棄的Entry。(一般情況就是由於弱引用被垃圾回收機制回收了)
  3. 到該位置的ThreadLocal等於我們要賦值的ThreadLocal,直接使用value覆蓋那個值。

有些讀者一定會驚訝,index++這是什麼操作,為什麼要這樣處理?
我們不得不去承認,衝突是無法完全避免。很有可能兩個不同的ThreadLocal 是同一個index,那麼這就是此處處理衝突的方式。

然後如果size大於了threshold(根據初始化中原始碼可知,threshold = len * 2 / 3),就會執行擴容操作。大致邏輯和HashMap的擴容操作相同,此處就不多加累述。如果好奇的同學可以自己檢視原始碼或者閱讀筆者關於HashMap的博文——HashMap原始碼分析

##ThreadLocalMap.getEntry()
這是ThreadLocalMap獲取內部例項的操作。

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

正常情況下比較簡單,如果table[i]不為null,並且該處的ThreadLocal 與匯入的引數ThreadLocal 相等,就直接返回value。我們主要看一下非理想情況下的getEntryAfterMiss()方法。

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

還是會從i開始,執行遍歷操作,如果table[i]處Entry的ThreadLocal 為空,表明可能Entry已經被回收或者本來就是設定的null,那麼這個節點就沒有意義,所以就刪掉。如果table[i]處Entry的ThreadLocal 等於匯入的引數ThreadLocal ,那麼就返回這個值。

#ThreadLocal
ThreadLocalMap我們瞭解的差不多了。知道了ThreadLocal儲存的資料結構,我們再去看ThreadLocal對其的操作就非常簡單了。
#ThreadLocal.Set()方法

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

首先獲取到當前執行緒,然後通過當前執行緒獲取到ThreadLocalMap ,如果ThreadLocalMap 不為null就將資料存進去,如果為null就建立ThreadLocalMap 。
我們先看一下getMap()方法。

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

它返回的是Thread.threadLocals這個變數,每個Thread都會都一個這樣的變數,用於儲存附屬於它的ThreadLocalMap 。而執行緒中所有建立的ThreadLocal 都會儲存在這個threadLocals中。這樣就保證了不同執行緒中的副本不會互相影響,因為ThreadLocalMap,即ThreadLocal的儲存容器是與執行緒本身繫結的。

#ThreadLocal.get()方法

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

首先還是通過執行緒獲取到ThreadLocalMap 。然後獲取到ThreadLocalMap 中的Entry ,再獲取Entry 中的value。如果獲取不到ThreadLocalMap 的話,就會初始化一個value返回。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;
    }

根據initialValue()這個方法進行value的初始化,但是預設情況下是null。然後會建立ThreadLocalMap ,並且把這個初始化的值放進去。
那麼既然預設返回值是null,又何必要有這個方法呢?我們可以看一下initialValue()的字首是protected ,所以說這個方法是為了繼承重寫用的。我們一般情況也用不到。

ThreadLocal知識點比較雜亂,筆者可能有地方沒有講清楚,歡迎讀者留言提醒。

相關文章