threadlocal中的hash
threadlocal的內存分佈如下圖所示。
每一個線程都有一個threadlocalmap(如上圖所示),threadlocalmap在threadlocal類中定義。threadlocalmap沒有繼承map而是自己寫了一套類似map的容器。threadlocalmap中存放的是一個數組,名字叫table,table中放着一個個的Entry,Entry是以key-value的方式存儲。key是threadlocal,value是線程內具體的值。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
問:怎麼確定key的存放位置呢?也就是threadlocal在table中的存放位置?
看源碼不難發現,是通過 int i = key.threadLocalHashCode & (len-1)方法獲得的,這裏採用的斐波那契散列方法。
(注:從 ThreadLocal 的實現看散列算法,這篇文章講的特別棒,嚴重推薦)
簡單說一下爲什麼用斐波那契散列方法呢?就是爲了讓存進去的值更加離散,爲什麼要讓存進去的值,更加離散呢?目的是爲了能更快找到存儲位置,通過魔法值和AtomicIntger的getAndAdd方法得到nextHashCode再與table的長度做與操作
threadLocalHashCode方法最終調用的是nextHashCode()方法。而nextHashCode()方法如下面代碼所示調用的是getAndAdd,這個方法的作用是讓當前線程的nextHashCode這個值與魔法值HASH_INCREMENT相加。每調用一次加一次魔法值。也就是線程中每添加一個threadlocal,AtomicInteger 類型的nextHashCode值就會增加一個HASH_INCREMENT。
魔法值:
ThreadLocal
中使用了斐波那契散列法,來保證哈希表的離散度。而它選用的乘數值即是2^32 * 黃金分割比
。
private static final int HASH_INCREMENT = 0x61c88647;
當thread的threadlocalmap的大小爲16時,每添加一個threadlocal,在原來nextHashCode的基礎上增加魔法值再與threadlocalmap的len-1做&操作後得到的如下所示的threadlocal在table中的索引。當擴容到32的時候時候每次得到的值就如下面所展示的順序了
16:0 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9
32: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
64:0 7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57
採用不同位數的jvm,魔法值也不一樣。如下所示
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
最後發現通過調用AtomicInteger類的getAndAdd方法來得到位置的。
每一個threadlocal都有AtomicInteger類型的nextHashCode用於存放本threadlocal的哈希值。有個這個值之後再與table的長度進行與操作,獲得threadlocal在table數組中的位置。
斐波那契散列法:
元素特徵轉變爲數組下標的方法就是散列法。斐波那契散列法是常用的一種方法。
斐波那契散列法:讓乘數乘上一個與它的位數相對應的斐波那契數,再進行散列。下圖就是我們根據位數常用的斐波那契數。
爲什麼叫斐波那契散列法呢?
因爲這個斐波那契數的值與0.618相關,也就是上圖所示的關係,那麼0.618與斐波那契有什麼關係呢?
斐波那契通過兔子的繁殖規律,發現了斐波那契數列,而斐波那契數列的前一項與後一項之比,隨着數值的增大無線接近黃金分割比例(0.618):
斐波那契數列又稱黃金分割數列:1 1 2 3 5 8 13 21 34 55 89 144 233 ……
1÷1=1,1÷2=0.5,2÷3=0.666...,3÷5=0.6,5÷8=0.625…………,55÷89=0.617977……………144÷233=0.618025…46368÷75025=0.6180339886…...
黃金分割比例:在線段上取一個點將線段分成一長一短的兩部分,長部分/總長=短部分/長部分,這個比例就是0.618
總之因爲用到了黃金分割比例值,所以叫斐波那契散列法。
解決哈希衝突
有了上面的理解,剛開始的時候認爲,每添加一個threadlocal,都會產生一個新的值,數組中被佔用的位置達到某個比例後會自動擴容,怎麼存在衝突呢?後來再次看源碼發現是會衝突的。
當向table不斷添加threadlocal,因爲threadlocal是弱引用,所以可能被回收,因爲沒有達到擴容的標準,所以當計算出最後的位置爲索引9後,再添加的時候就要向索引0位置添加,但是有可能索引0位置的threadlocal沒有被回收,所以就出現了哈希衝突。這個時候就需要向下循環,看索引爲1的位置是否有值,如果沒有添加,如果有繼續循環知道找到空的位置爲止。這個方法叫開放地址法。
threadlocal中set源碼:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//解決hash衝突的方法
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();
}