好像今天沒有什么源碼讀,那么就來看看java的這兩種HashMap有啥不一樣的地方吧,在這之前先普及一下HashMap的一些基本知識:
(1)放入HashMap的元素是key-value對。
(2)底層說白了就是以前數(shù)據(jù)結(jié)構(gòu)課程講過的散列結(jié)構(gòu)。
(3)要將元素放入到hashmap中,那么key的類型必須要實現(xiàn)實現(xiàn)hashcode方法,默認(rèn)這個方法是根據(jù)對象的地址來計算的,具體我也記不太清楚了,接著還必須覆蓋對象的equal方法。
用一張圖來表示一下散列結(jié)構(gòu)吧:

在這里hashCode函數(shù)就是用于確定當(dāng)前key應(yīng)該放在hash桶里面的位置,這里hash桶可以看成是一個數(shù)組,最簡單的通過一些取余的方法就能用來確認(rèn)key應(yīng)該擺放的位置,而equal函數(shù)則是為了與后面的元素之間判斷重復(fù)。
好了,這里我們接下來來看看java的這兩種類庫的用法吧:
由于他們都實現(xiàn)了Map接口,將元素放進(jìn)去的方法就是put(a,b),這里我們先來分析比較簡單的HashMap吧:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key); //獲取當(dāng)前key的hash值 int i = indexFor(hash, table.length); //返回在hash桶里面的位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //遍歷當(dāng)前hansh桶后面的元素 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果有相同的key,那么需要替換value V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; //返回以前的value } } modCount++; addEntry(hash, key, value, i); //放入entry return null; } |
這個函數(shù)其實本身還是很簡單的,首先通過hash函數(shù)獲取當(dāng)前key的hash值,不過這里需要注意的是,對hashCode方法返回的值HashMap本身還會進(jìn)行一些處理,具體什么樣子的就不細(xì)說了,然后再調(diào)用indexFor方法用于確定當(dāng)前key應(yīng)該屬于當(dāng)前Hash桶的位置,接著就是遍歷當(dāng)前桶后面的鏈表了,這里equal方法就派上用場了,這里看到如果equal是相等的話,那么就直接用新的value來替換原來的value就好了。。。
當(dāng)然最多的情況還是,桶后面的鏈表沒有與當(dāng)前的key相同的,那么這個時候就需要調(diào)用addEntry方法,將要加入的key-value放入到當(dāng)前的結(jié)構(gòu)中了,那么接下來來看看這個方法的定義吧:
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); //相當(dāng)于重新設(shè)置hash桶 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); //創(chuàng)建新的entry,并將它加入到當(dāng)前的桶后面的鏈表中 } |
其實這個方法很簡單,首先來判斷當(dāng)前的桶的大小,如果覺得太小的話,那么需要擴(kuò)充當(dāng)前桶的大小,這樣可以讓添加元素存放的更離散化一些,優(yōu)化擦入和尋找的效率。
然后就是創(chuàng)建一個新的entry,用于保存要擦入的key和value,然后再將其鏈到應(yīng)該放的桶的鏈表上就好了。。
好了,到這里位置,整個HashMap的擦入元素的過程就已經(jīng)看的很清楚了,在整個這個過程中沒有看到有加鎖的過程,因此可以說明HashMap是不支持并發(fā)的,不是線程安全的,在并發(fā)的環(huán)境下使用會產(chǎn)生一些不一致的問題。。。
因此java新的concurrent類庫中就有了ConcurrentHashMap用于在并發(fā)環(huán)境中使用。。
那么我們再來看看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 //用于獲取相應(yīng)的片段 s = ensureSegment(j); //這里表示沒有這個片段,那么需要創(chuàng)建這個片段 return s.put(key, hash, value, false); //這里就有分段加鎖的策略 } |
這里剛開始跟HashMap都差不太多吧,無非是先獲取當(dāng)前key的hash值,但是接下來進(jìn)行的工作就不太一樣了,這里就有了一個分段的概念:
ConcurrentHashMap將整個Hash桶進(jìn)行了分段,也就是將這個大的數(shù)組分成了幾個小的片段,而且每個小的片段上面都有鎖存在,那么在擦入元素的時候就需要先找到應(yīng)該插入到哪一個片段,然后再在這個片段上面進(jìn)行擦入,而且這里還需要獲取鎖。。。。
那我們來看看這個segment的put方法吧:
final V put(K key, int hash, V value, boolean onlyIfAbsent) { //這里的鎖是計數(shù)鎖,同一個鎖可以被同一個線程獲取多次,但是不能被不同的線程獲取 HashEntry<K,V> node = tryLock() ? null : //如果獲取了當(dāng)前的segment的鎖,那么node為null,待會自己分配就好了 scanAndLockForPut(key, hash, value); //如果沒有加上鎖,那么等吧,有可能的話還要分配entry,反正有時間干嘛不多做一些事情 V oldValue; try { //這里表示已經(jīng)獲取了鎖,那么將在相應(yīng)的位置放入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;;) { //從當(dāng)前的第一個元素開始 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) //如果元素太多了,那么需要重新調(diào)整當(dāng)前的hash結(jié)構(gòu),讓桶變多一些,這樣元素放的更離散一些 rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); //這里必須要在finally里面釋放已經(jīng)獲取的鎖,這樣才能保證鎖一定會被釋放 } return oldValue; }
|
其實在這里ConcurrentHashMap和HashMap的區(qū)別就已經(jīng)很明顯了:
(1)ConcurrentHashMap對整個桶數(shù)組進(jìn)行了分段,而HashMap則沒有
(2)ConcurrentHashMap在每一個分段上都用鎖進(jìn)行保護(hù),從而讓鎖的粒度更精細(xì)一些,并發(fā)性能更好,而HashMap沒有鎖機(jī)制,不是線程安全的。。。
最后用一張圖來表來說明一下ConcurrentHashMap吧:

最后,在并發(fā)的情況下,要么使用concurrent類庫中提供的容器,要么就需要自己來管理數(shù)據(jù)的同步問題了。。。