散列表(hash table)咱們平時也叫它哈希表或者Hash表,它用的是數組支持按照下標隨機訪問數據的特性,因此散列表其實就是數組的一種擴展,由數組演化而來。能夠說,沒有數組就沒有散列表。java
好比咱們有100件商品,編號沒有規律的4位數字,如今咱們想要經過編號快速獲取商品信息,如何作呢?咱們能夠將這100件商品信息放到數組裏,經過 商品編號%100這樣的方式獲得一個值,值爲1的商品放到數組中下標爲1的位置,值爲2的商品,咱們放到數組中下標爲2的位置。以此類推,編號爲K的選手放到數組中下標爲K的位置。由於商品編號經過散列函數(編號%100)跟數據下標一一對應,因此但咱們須要查詢編號爲x的商品信息的時候,咱們使用一樣的方式,將編號轉換爲數組下標,就能夠從對應的數組下標的位置取出數據。 這就是散列的典型思想。算法
咱們經過上面的例子能夠得出這樣規律:散列表用的就是數組支持按照下標隨機訪問的時候,時間複雜度是 O(1) 的特性。經過散列函數(商品編號%100)把元素的鍵值映 射爲下標,而後將數據存儲在數組中對應下標的位置。當咱們按照鍵值查詢元素時,咱們用一樣的散列函數,將鍵值轉化數組下標,從對應的數組下標的位置取 數據。數組
一說到散列(或者叫作hash表),你們更熟悉的是HashMap或者LinkedHashMap,而今天的主角是ThreadLocalMap,它是ThreadLocal中的一個內部類。分析ThreadLocal源碼的時候不可能繞過它。bash
因爲哈希表使用了數組,不管hash函數如何設計都無可避免存在hash衝突。上面的例子若是兩件商品的id分別是1001和1101,那麼他們的數據就會就會被放到數組的同一個位置,出現了衝突數據結構
鴿巢原理,又名狄利克雷抽屜原理、鴿籠原理。其中一種簡單的表述法爲:如有n個籠子和n+1只鴿子,全部的鴿子都被關在鴿籠裏,那麼至少有一個籠子有至少2只鴿子ide
ThreadLocalMap做爲hash表的一種實現方式,它是使用什麼方式來解決衝突的呢?它使用了開放尋址法來解決這個問題。函數
開放尋址法的核心是若是出現了散列衝突,就從新探測一個空閒位置,將其插入。當咱們往散列表中插入數據時,若是某個數據通過散列函數散列以後,存儲位置已經被佔用了,咱們就從當前位置開始,依次日後查找,看是否有空閒位置,直到找到爲止。源碼分析
從圖中能夠看出,散列表的大小爲 10 ,在元素 x 插入散列表以前,已經 6 個元素插入到散列表中。 x 通過 Hash 算法以後,被散列到位置下標爲 7 的位置,可是這個位置已經有數據了,因此就產生了衝突。因而咱們就順序地日後一個一個找,看有沒有空閒的位置,遍歷到尾部都沒有找到空閒的位置,因而咱們再從表頭開始找,直到找到空閒位置 2 ,因而將其插入到這個位置。性能
在散列表中查找元素的過程有點兒相似插入過程。咱們經過散列函數求出要查找元素的鍵值對應的散列值,而後比較數組中下標爲散列值的元素和要查找的元素。若是相等,則說明就是咱們要找的元素;不然就順序日後依次查找。若是遍歷到數組中的空閒位置,尚未找到,就說明要查找的元素並無在散列表中。this
ThreadLocalMap跟數組同樣,不只支持插入、查找操做,還支持刪除操做。對於使用線性探測法解決衝突的散列表,刪除操做稍微有些特別。咱們不能單純地把要刪除的元素設置爲空。
還記得咱們剛講的查找操做嗎?在查找的時候,一旦咱們經過線性探測方法,找到一個空閒位置,咱們就能夠認定散列表中不存在這個數據。可是,若是這個空閒位置是咱們後來刪除的,就會致使原來的查找算法失效。原本存在的數據,會被認定爲不存在。這個問題如何解決呢?
咱們能夠在刪除元素以後,將以後不爲null的數據rehash,這樣就不會影響查詢的邏輯
另外一種方法是:能夠將刪除的元素,特殊標記爲 deleted 。當線性探測查找的時候,遇到標記爲 deleted 的空間,並非停下來,而是繼續往下探測
這裏解釋下rehash的過程:當刪除元素8的時候,先把下標爲8的值設置爲null,而後將其後面不爲空的數組元素rehash。好比8後面的元素是9,其本來應該的位置(9%10=9)就在9因此不移動。下一個元素是19,應該在下標爲9的位置,可是已經被佔用了,因此就找下一個空閒的位置,下標爲3的位置是空閒的,放入tab[3]。接着下一個元素1就在tab[1]不移動, 元素7的位置在tab[7],由於已經被佔用,放入下一個空閒位置tab[8]。下一個元素仍然是19,這裏因爲tab[9]已經被佔用,因此放入下一個空閒位置tab[0]。接着最後一個元素4位置就在tab[4],因此不移動。元素4的下一個位置爲空,整個rehash過程結束。
你可能已經發現了,線性探測法其實存在很大問題。當散列表中插入的數據愈來愈多時,散列衝突發生的可能性就會愈來愈大,空閒位置會愈來愈少,線性探測的時間就會愈來愈久。極端狀況下,咱們可能須要探測整個散列表,因此最壞狀況下的時間複雜度爲 O(n) 。同理,在刪除和查找時,也有可能會線性探測整張散列表,才能找到要查找或者刪除的數據。
無論採用哪一種探測方法,hash函數設計得在合理,當散列表中空閒位置很少的時候,散列衝突的機率就會大大提升。爲了儘量保證散列表的操做效率,通常狀況下,咱們會盡量保證散列表中有必定比例的空閒槽位。咱們用裝載因子(load factor)來表示空位的多少。
裝載因子的計算公式是:散列表的裝載因子=填入表中的元素個數/散列表的長度 裝載因子越大,說明空閒位置越少,衝突越多,散列表的性能會降低。
ThreadLocal的核心數據結構是ThreadLocalMap,它的數據結構以下:
static class ThreadLocalMap {
// 這裏的entry繼承WeakReference了
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始化容量,必須是2的n次方
private static final int INITIAL_CAPACITY = 16;
// entry數組,用於存儲數據
private Entry[] table;
// map的容量
private int size = 0;
// 數據量達到多少進行擴容,默認是 table.length * 2 / 3
private int threshold;
複製代碼
從ThreadLocalMap的定義能夠看出Entry的key就是ThreadLocal,而value就是值。同時,Entry也繼承WeakReference,因此說Entry所對應key(ThreadLocal實例)的引用爲一個弱引用。並且定義了裝載因子爲數組長度的三分之二。
private void set(ThreadLocal<?> key, Object value) {
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();
// key存在則直接覆蓋
if (k == key) {
e.value = value;
return;
}
// key不存在,說明以前的ThreadLocal對象被回收了
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 不存在也沒有舊元素,就建立一個
tab[i] = new Entry(key, value);
int sz = ++size;
// 清除舊的槽(entry不爲空,可是ThreadLocal爲空),而且當數組中元素大於閾值就rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
expungeStaleEntries();
// 擴容
if (size >= threshold - threshold / 4)
resize();
}
複製代碼
上面源碼的主要步驟以下:
不管是replaceStaleEntry()方法仍是cleanSomeSlots()方法,最重要的方法調用是expungeStaleEntry(),你能夠在ThreadLocalMap中的get,set,remove都能發現調用它的身影。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 刪除對應位置的entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// rehash過程,直到entry爲null
for (i = nextIndex(staleSlot, len);(e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// k爲空,證實已經被垃圾回收了
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
// 判斷當前元素是否處於"真正"應該待的位置
if (h != i) {
tab[i] = null;
// 線性探測
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
複製代碼
上面rehash的代碼結合文章開頭的說明理解起來更是容易,當從ThreadLocalMap新增,獲取,刪除的時候都會根據條件進行rehash,條件以下
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;
// ThreadLocal爲空,須要刪除過時元素,同時進行rehash
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
複製代碼
線性探測法貫穿了get,set的全部流程,理解了原理在看代碼就很簡單了。
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
複製代碼
remove的時候回刪除舊的entry,而後進行rehash.
public class Counter {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
public Integer initialValue() {
return 0;
}
};
public int nextInt(){
seqCount.set(seqCount.get() + 1);
return seqCount.get();
}
public static void main(String[] args){
Counter seqCount = new Counter();
CountThread thread1 = new CountThread(seqCount);
CountThread thread2 = new CountThread(seqCount);
CountThread thread3 = new CountThread(seqCount);
CountThread thread4 = new CountThread(seqCount);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
private static class CountThread extends Thread{
private Counter counter;
CountThread(Counter counter){
this.counter = counter;
}
@Override
public void run() {
for(int i = 0 ; i < 3 ; i++){
System.out.println(Thread.currentThread().getName() + " seqCount :" + counter.nextInt());
}
}
}
}
複製代碼
運行效果以下:
Thread-3 seqCount :1
Thread-0 seqCount :1
Thread-3 seqCount :2
Thread-0 seqCount :2
Thread-0 seqCount :3
Thread-2 seqCount :1
Thread-2 seqCount :2
Thread-1 seqCount :1
Thread-3 seqCount :3
Thread-1 seqCount :2
Thread-1 seqCount :3
Thread-2 seqCount :3
複製代碼
ThreadLocal 實際上是爲每一個線程都提供一份變量的副本, 從而實現同時訪問而不受影響。從這裏也看出來了它和synchronized之間的應用場景不一樣, synchronized是爲了讓每一個線程對變量的修改都對其餘線程可見, 而 ThreadLocal 是爲了線程對象的數據不受其餘線程影響, 它最適合的場景應該是在同一線程的不一樣開發層次中共享數據。