threadlocal中hash

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

參考文章:

散列算法和hash函數

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章