ThreadLocal的介紹與運用( 六 )

5.2 hash沖突的解決ThreadLocal使用的是自定義的ThreadLocalMap , 接下來我們來探究一下ThreadLocalMap的hash沖突解決方式 。
(1) 先回顧ThreadLocal的set() 方法
public void set(T value) {Thread t = Thread.currentThread();ThreadLocal.ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}ThreadLocal.ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);}

  • 代碼很簡單,獲取當前線程,并獲取當前線程的ThreadLocalMap實例(從getMap(Thread t)中很容易看出來) 。
  • 如果獲取到的map實例不為空,調用map.set()方法,否則調用構造函數 ThreadLocal.ThreadLocalMap(this, firstValue)實例化map 。
可以看出來線程中的ThreadLocalMap使用的是延遲初始化,在第一次調用get()或者set()方法的時候才會進行初始化 。
(2) 下面來看看構造函數ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//初始化tabletable = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];//計算索引int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//設置值table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);size = 1;//設置閾值setThreshold(INITIAL_CAPACITY);}主要說一下計算索引,firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1) 。
  • 關于& (INITIAL_CAPACITY - 1),這是取模的一種方式,對于2的冪作為模數取模,用此代替%(2^n) , 這也就是為啥容量必須為2的冥 , 在這個地方也得到了解答 。
  • 關于firstKey.threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}private static AtomicInteger nextHashCode =new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;? 這里定義了一個AtomicInteger類型 , 每次獲取當前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,這個值和斐波那契散列有關(這是一種乘數散列法,只不過這個乘數比較特殊,是32位整型上限2^32-1乘以黃金分割比例0.618....的值2654435769,用有符號整型表示就是-1640531527,去掉符號后16進制表示為0x61c88647),其主要目的就是為了讓哈希碼能均勻的分布在2的n次方的數組里, 也就是Entry[] table中 , 這樣做可以盡量避免hash沖突 。
(3) ThreadLocalMap中的set()
? ThreadLocalMap使用開發地址-線性探測法來解決哈希沖突,線性探測法的地址增量di = 1, 2, ... 其中,i為探測次數 。該方法一次探測下一個地址 , 直到有空的地址后插入,若整個空間都找不到空余的地址,則產生溢出 。假設當前table長度為16,也就是說如果計算出來key的hash值為14,如果table[14]上已經有值,并且其key與當前key不一致,那么就發生了hash沖突,這個時候將14加1得到15,取table[15]進行判斷,這個時候如果還是沖突會回到0,取table[0],以此類推,直到可以插入 。
按照上面的描述,可以把table看成一個環形數組
先看一下線性探測相關的代碼,從中也可以看出來table實際是一個環:
/*** 獲取環形數組的下一個索引*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}/*** 獲取環形數組的上一個索引*/private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}ThreadLocalMap的set()代碼如下:
private void set(ThreadLocal<?> key, Object value) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;//計算索引,上面已經有說過 。int i = key.threadLocalHashCode & (len-1);/*** 根據獲取到的索引進行循環,如果當前索引上的table[i]不為空 , 在沒有return的情況下,* 就使用nextIndex()獲取下一個(上面提到到線性探測法) 。*/for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//table[i]上key不為空,并且和當前key相同,更新valueif (k == key) {e.value = https://www.huyubaike.com/biancheng/value;return;}/*** table[i]上的key為空,說明被回收了* 這個時候說明改table[i]可以重新使用 , 用新的key-value將其替換,并刪除其他無效的entry*/if (k == null) {replaceStaleEntry(key, value, i);return;}}

推薦閱讀