1. 什麼是Hash表java
hash函數就是根據key計算出應該存儲地址的位置,而哈希表是基於哈希函數創建的一種查找表編程
2. hash函數設計的考慮因素數組
3.哈希衝突的解決方案安全
無論hash函數設計的如何巧妙,總會有特殊的key致使hash衝突,特別是對動態查找表來講。hash函數解決衝突的方法有如下幾個經常使用的方法數據結構
A.開放定製法(線性探索)
B.鏈地址法(HashMap)
C.公共溢出區法創建一個特殊存儲空間,專門存放衝突的數據。此種方法適用於數據和衝突較少的狀況。
D.再散列法(布隆過濾器)準備若干個hash函數,若是使用第一個hash函數發生了衝突,就使用第二個hash函數,第二個也衝突,使用第三個…… 多線程
當一個關鍵字和另外一個關鍵字發生衝突時,使用某種探測技術在Hash表中造成一個探測序列,而後沿着這個探測序列依次查找下去,當碰到一個空的單元時,則插入其中。基本公式爲:hash(key) = (hash(key)+di)mod TableSize。其中di爲增量序列,TableSize爲表長。根據di的不一樣咱們又能夠分爲線性探測,平方(二次)探測,雙散列探測。 併發
1)線性探測
以增量序列 1,2,……,(TableSize -1)循環試探下一個存儲地址,即di = i。若是table[index+di]爲空則進行插入,反之試探下一個增量。可是線性探測也有弊端,就是會形成元素彙集現象,下降查找效率。具體例子以下圖: 函數
特別對於開放定址法的刪除操做,不能簡單的進行物理刪除,由於對於同義詞來講,這個地址可能在其查找路徑上,若物理刪除的話,會中斷查找路徑,故只能設置刪除標誌。高併發
//插入函數,利用線性探測法 bool Insert_Linear_Probing(int num){ //哈希表已經被裝滿,則不在填入 if(this->size == this->length){ return false; } int index = this->hash(num); if(this->data[index] == MAX){ this->data[index] = num; }else{ int i = 1; //尋找合適位置 while(this->data[(index+i)%this->length] != MAX){ i++; } index = (index+i)%this->length; this->data[index] = num; } if(this->delete_flag[index] == 1){//以前設置爲刪除 this->delete_flag[index] = 0; } this->size++; return true; }
HashMap便是採用了鏈地址法,也就是數組+鏈表的方式,HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每個Entry包含一個key-value鍵值對。 性能
//HashMap的主幹數組,能夠看到就是一個Entry數組,初始值爲空數組{},主幹數組的長度必定是2的次冪,至於爲何這麼作,後面會有詳細分析。 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry是HashMap中的一個靜態內部類。代碼以下
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構 int hash;//對key的hashcode值進行hash運算後獲得的值,存儲在Entry,避免重複計算 /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
因此,HashMap的總體結構以下
簡單來講,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。
ConcurrentHashMap是Java併發包中提供的一個線程安全且高效的HashMap實現,ConcurrentHashMap在併發編程的場景中使用頻率很是之高,下面咱們來分析下ConcurrentHashMap的實現原理,並對其實現原理進行分析 。
衆所周知,哈希表是種很是高效,複雜度爲O(1)的數據結構,在Java開發中,咱們最多見到最頻繁使用的就是HashMap和HashTable,可是在線程競爭激烈的併發場景中使用都不夠合理。
HashMap :先說HashMap,HashMap是線程不安全的,在併發環境下,可能會造成環狀鏈表(多線程擴容時可能形成),致使get操做時,cpu空轉, 因此,在併發環境中使用HashMap是很是危險的。
HashTable : HashTable和HashMap的實現原理幾乎同樣,差異無非是1.HashTable不容許key和value爲null;2.HashTable是線程安全的。可是HashTable線程安全的策略實現代價卻太大了,簡單粗暴,get/put全部相關操做都是synchronized的,這至關於給整個哈希表加了一把大鎖,多線程訪問時候,只要有一個線程訪問或操做該對象,那其餘線程只能阻塞,至關於將全部的操做串行化,在競爭激烈的併發場景中性能就會很是差。
HashTable性能差主要是因爲全部操做須要競爭同一把鎖,而若是容器中有多把鎖,每一把鎖鎖一段數據比喻[11],這樣在多線程訪問時不一樣段的數據時,就不會存在鎖競爭了,這樣即可以有效地提升併發效率。這就是ConcurrentHashMap所採用的"分段鎖"思想。java1.7後的CHM中把每一個數組叫Segment,每一個segment下面存的是默認16段的Hashhenery,Hashhenery解決充突是在Hashhenery下面掛載鏈表,咱們就畫圖說明下分段鎖
ConcurrentHashMap
初始化時,計算出Segment
數組的大小ssize
和每一個Segment
中HashEntry
數組的大小cap
,並初始化Segment
數組的第一個元素;其中ssize
大小爲2的冪次方,默認爲16,cap
大小也是2的冪次方,最小值爲2,最終結果根據初始化容量initialCapacity
進行計算,計算過程以下
if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1;
由於Segment
繼承了ReentrantLock,全部segment是線程安全的,可是在1.8中放棄了Segment
分段鎖的設計,使用的是Node
+CAS
+Synchronized
來保證線程安全性,並且這樣設計的好處是層級下降了,鎖的粒度更小了,能夠說是一種優化,比喻鎖的是2,那麼他鎖的就只是發生衝突的2下面的鏈表,而不像1.7樣,是鎖整個HashEntry;並且1.8中對鏈表的長度進行了優化,在1.7的鏈表中鏈表查詢的複雜度是O(n),可是在1.8中爲了解決這問題引入了紅黑樹,在1.8中當咱們鏈表長度大於8時而且數組長度大於64時,就會發生一個鏈表的轉換,會把單向鏈表轉換成紅黑樹。
在1.7 中當執行put
方法插入數據的時候,根據key的hash值,在Segment
數組中找到對應的位置若是當前位置沒有值,則經過CAS進行賦值,接着執行Segment
的put
方法經過加鎖機制插入數據;假若有線程AB同時執行相同Segment
的put
方法
線程A 執行tryLock方法成功獲取鎖,而後把HashEntry對象插入到相應位置 線程B 嘗試獲取鎖失敗,則執行scanAndLockForPut()方法,經過重複執行tryLock()方法嘗試獲取鎖 在多處理器環境重複64次,單處理器環境重複1次,當執行tryLock()方法的次數超過上限時,則執行lock()方法掛起線程B 當線程A執行完插入操做時,會經過unlock方法施放鎖,接着喚醒線程B繼續執行
但在1.8 中執行put
方法插入數據的時候,根據key的hash值在Node
數組中找到相應的位置若是當前位置的 Node
尚未初始化,則經過CAS插入數據
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //若是當前位置的`Node`尚未初始化,則經過CAS插入數據 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin }
若是當前位置的Node已經有值,則對該節點加synchronized
鎖,而後從該節點開始遍歷,直到插入新的節點或者更新新的節點
if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } }
若是當前節點是TreeBin
類型,說明該節點下的鏈表已經進化成紅黑樹結構,則經過putTreeVal
方法向紅黑樹中插入新的節點
else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } }
若是binCount
不爲0,說明put
操做對數據產生了影響,若是當前鏈表的節點個數達到了8個,則經過treeifyBin
方法將鏈表轉化爲紅黑樹