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