java的HashMap與ConcurrentHashMap
好像今天沒有什么源碼讀,那么就來看看java的這兩種HashMap有啥不一樣的地方吧,在這之前先普及一下HashMap的一些基本知識:
(1)放入HashMap的元素是key-value對。
(2)底層說白了就是以前數據結構課程講過的散列結構。
(3)要將元素放入到hashmap中,那么key的類型必須要實現實現hashcode方法,默認這個方法是根據對象的地址來計算的,具體我也記不太清楚了,接著還必須覆蓋對象的equal方法。
用一張圖來表示一下散列結構吧:
在這里hashCode函數就是用于確定當前key應該放在hash桶里面的位置,這里hash桶可以看成是一個數組,最簡單的通過一些取余的方法就能用來確認key應該擺放的位置,而equal函數則是為了與后面的元素之間判斷重復。
好了,這里我們接下來來看看java的這兩種類庫的用法吧:
由于他們都實現了Map接口,將元素放進去的方法就是put(a,b),這里我們先來分析比較簡單的HashMap吧:
public V put(K key, V value) { modCount++; |
這個函數其實本身還是很簡單的,首先通過hash函數獲取當前key的hash值,不過這里需要注意的是,對hashCode方法返回的值HashMap本身還會進行一些處理,具體什么樣子的就不細說了,然后再調用indexFor方法用于確定當前key應該屬于當前Hash桶的位置,接著就是遍歷當前桶后面的鏈表了,這里equal方法就派上用場了,這里看到如果equal是相等的話,那么就直接用新的value來替換原來的value就好了。。。
當然最多的情況還是,桶后面的鏈表沒有與當前的key相同的,那么這個時候就需要調用addEntry方法,將要加入的key-value放入到當前的結構中了,那么接下來來看看這個方法的定義吧:
void addEntry(int hash, K key, V value, int bucketIndex) { createEntry(hash, key, value, bucketIndex); //創建新的entry,并將它加入到當前的桶后面的鏈表中 |
其實這個方法很簡單,首先來判斷當前的桶的大小,如果覺得太小的話,那么需要擴充當前桶的大小,這樣可以讓添加元素存放的更離散化一些,優化擦入和尋找的效率。
然后就是創建一個新的entry,用于保存要擦入的key和value,然后再將其鏈到應該放的桶的鏈表上就好了。。
好了,到這里位置,整個HashMap的擦入元素的過程就已經看的很清楚了,在整個這個過程中沒有看到有加鎖的過程,因此可以說明HashMap是不支持并發的,不是線程安全的,在并發的環境下使用會產生一些不一致的問題。。。
因此java新的concurrent類庫中就有了ConcurrentHashMap用于在并發環境中使用。。
那么我們再來看看ConcurrentHashMap的put操作是怎么搞的吧:
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); //獲取hash值 int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment //用于獲取相應的片段 s = ensureSegment(j); //這里表示沒有這個片段,那么需要創建這個片段 return s.put(key, hash, value, false); //這里就有分段加鎖的策略 } |
這里剛開始跟HashMap都差不太多吧,無非是先獲取當前key的hash值,但是接下來進行的工作就不太一樣了,這里就有了一個分段的概念:
ConcurrentHashMap將整個Hash桶進行了分段,也就是將這個大的數組分成了幾個小的片段,而且每個小的片段上面都有鎖存在,那么在擦入元素的時候就需要先找到應該插入到哪一個片段,然后再在這個片段上面進行擦入,而且這里還需要獲取鎖。。。。
那我們來看看這個segment的put方法吧:
final V put(K key, int hash, V value, boolean onlyIfAbsent) { //這里的鎖是計數鎖,同一個鎖可以被同一個線程獲取多次,但是不能被不同的線程獲取 HashEntry<K,V> node = tryLock() ? null : //如果獲取了當前的segment的鎖,那么node為null,待會自己分配就好了 scanAndLockForPut(key, hash, value); //如果沒有加上鎖,那么等吧,有可能的話還要分配entry,反正有時間干嘛不多做一些事情 V oldValue; try { //這里表示已經獲取了鎖,那么將在相應的位置放入entry HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash; HashEntry<K,V> first = entryAt(tab, index); //找到存放entry的桶,然后獲取第一個entry for (HashEntry<K,V> e = first;;) { //從當前的第一個元素開始 if (e != null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { //如果key相等,那么直接替換元素 oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { if (node != null) node.setNext(first); else node = new HashEntry<K,V>(hash, key, value, first); int c = count + 1; if (c > threshold && tab.length < MAXIMUM_CAPACITY) //如果元素太多了,那么需要重新調整當前的hash結構,讓桶變多一些,這樣元素放的更離散一些 rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); //這里必須要在finally里面釋放已經獲取的鎖,這樣才能保證鎖一定會被釋放 } return oldValue; } |
其實在這里ConcurrentHashMap和HashMap的區別就已經很明顯了:
(1)ConcurrentHashMap對整個桶數組進行了分段,而HashMap則沒有
(2)ConcurrentHashMap在每一個分段上都用鎖進行保護,從而讓鎖的粒度更精細一些,并發性能更好,而HashMap沒有鎖機制,不是線程安全的。。。
最后用一張圖來表來說明一下ConcurrentHashMap吧:
最后,在并發的情況下,要么使用concurrent類庫中提供的容器,要么就需要自己來管理數據的同步問題了。。。