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>"。