Java多執行緒9:ThreadLocal原始碼剖析

五月的倉頡發表於2015-10-04

ThreadLocal原始碼剖析

ThreadLocal其實比較簡單,因為類裡就三個public方法:set(T value)、get()、remove()。先剖析原始碼清楚地知道ThreadLocal是幹什麼用的、再使用、最後總結,講解ThreadLocal採取這樣的思路。

 

三個理論基礎

在剖析ThreadLocal原始碼前,先講一下ThreadLocal的三個理論基礎:

1、每個執行緒都有一個自己的ThreadLocal.ThreadLocalMap物件

2、每一個ThreadLocal物件都有一個迴圈計數器

3、ThreadLocal.get()取值,就是根據當前的執行緒,獲取執行緒中自己的ThreadLocal.ThreadLocalMap,然後在這個Map中根據第二點中迴圈計數器取得一個特定value值

 

兩個數學問題

1、ThreadLocal.ThreadLocalMap規定了table的大小必須是2的N次冪

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

因為從計算機的角度講,對位操作的效率比數學運算要高

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

比方說當前table長度是16,那麼16-1=15,也就是二進位制的1111。現在有一個數字是23,也就是二進位制的00010111。23%16=7,看下&運算:

00010111

      &

00001111=

00000111

00000111也就是7,和取模運算結果一樣,效率反而高。

2、Hash增量設定為0x61c88647,也就是說ThreadLocal通過取模的方式取得table的某個位置的時候,會在原來的threadLocalHashCode的基礎上加上0x61c88647

/**
 * 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;

雖然不知道這是為什麼,但是從對table.length取模的角度來看,試了一下length為16和32的情況:

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 

這樣一來避免了Hash衝突,二來相鄰的兩個數字都比較分散。而且在2的N次冪過後,又從第一個數字開始迴圈了,這意味,threadLocalHashCode可以從任何地方開始

有了這些理論基礎,下面可以看一下ThreadLocal幾個方法的實現原理。

 

set(T value)

一點點看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 getMap(Thread t) {
        return t.threadLocals;
    }
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

和前面講的一樣:

1、取得當前的執行緒

2、獲取執行緒裡面的ThreadLocal.ThreadLocalMap

3、看這個ThreadLocal.ThreadLocalMap是否存在,存在就設定一個值,不存在就給執行緒建立一個ThreadLocal.ThreadLocalMap

第三點有兩個分支,先看簡單的建立Map的分支:

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
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 final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT); 
    }
private static AtomicInteger nextHashCode = 
    new AtomicInteger();

這個Map中並沒有next節點,所以,不得不說ThreadLocalMap是一個有點誤導性的名字,它雖然叫做Map,但其實儲存的方式不是連結串列法而是開地址法。看到設定table中的位置的時候,都把一個static的nextHashCode累加一下,這意味著,set的同一個value,可能在每個ThreadLocal.ThreadLocalMap中的table中的位置都不一樣,不過這沒關係。

OK,看完了建立的分支,看一下設定的分支:

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();
        }
private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

理一下邏輯,設定的時候做了幾步:

1、先對ThreadLocal裡面的threadLocalHashCode取模獲取到一個table中的位置

2、這個位置上如果有資料,獲取這個位置上的ThreadLocal

(1)判斷一下位置上的ThreadLocal和我本身這個ThreadLocal是不是一個ThreadLocal,是的話資料就覆蓋,返回

(2)不是同一個ThreadLocal,再判斷一下位置上的ThreadLocal是是不是空的,這個解釋一下。Entry是ThreadLocal弱引用,"static class Entry extends WeakReference<ThreadLocal>",有可能這個ThreadLocal被垃圾回收了,這時候把新設定的value替換到當前位置上,返回

(3)上面都沒有返回,給模加1,看看模加1後的table位置上是不是空的,是空的再加1,判斷位置上是不是空的...一直到找到一個table上的位置不是空的為止,往這裡面塞一個value。換句話說,當table的位置上有資料的時候,ThreadLocal採取的是辦法是找最近的一個空的位置設定資料

 

get()

如果理解清楚了set(T value),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();
    }
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;
        }

理一下步驟:

1、獲取當前執行緒

2、嘗試去當前執行緒中拿它的ThreadLocal.ThreadLocalMap

3、當前執行緒中判斷是否有ThreadLocal.ThreadLocalMap

(1)有就嘗試根據當前ThreadLocal的threadLocalHashCode取模去table中取值,有就返回,沒有就給模加1繼續找,這和設定的演算法是一樣的

(2)沒有就呼叫set方法給當前執行緒ThreadLocal.ThreadLocalMap設定一個初始值

 

remove()

remove()方法就非常簡單了:

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

取得當前執行緒的ThreadLocal.ThreadLocalMap,如果有ThreadLocal.ThreadLocalMap,找到對應的Entry,移除掉就好了。

 

總結

上面分析了這麼多原始碼,是比較細節地來看ThreadLocal了。對這些內容做一個總結,ThreadLocal的原理簡單說應該是這樣的:

  1. ThreadLocal不需要key,因為執行緒裡面自己的ThreadLocal.ThreadLocalMap不是通過連結串列法實現的,而是通過開地址法實現的
  2. 每次set的時候往執行緒裡面的ThreadLocal.ThreadLocalMap中的table陣列某一個位置塞一個值,這個位置由ThreadLocal中的threadLocaltHashCode取模得到,如果位置上有資料了,就往後找一個沒有資料的位置
  3. 每次get的時候也一樣,根據ThreadLocal中的threadLocalHashCode取模,取得執行緒中的ThreadLocal.ThreadLocalMap中的table的一個位置,看一下有沒有資料,沒有就往下一個位置找
  4. 既然ThreadLocal沒有key,那麼一個ThreadLocal只能塞一種特定資料。如果想要往執行緒裡面的ThreadLocal.ThreadLocalMap裡的table不同位置塞資料 ,比方說想塞三種String、一個Integer、兩個Double、一個Date,請定義多個ThreadLocal,ThreadLocal支援泛型"public class ThreadLocal<T>"。 

相關文章