淺析ConcurrentHashMap

1、導論

這些天一直在看關於多線程和高併發的書籍,也對jdk中的併發措施瞭解了些許,看到concurrentHashMap的時候感受知識點很亂,有必要寫篇博客整理記錄一下。java

當資源在多線程下共享時會產生一些邏輯問題,這個時候類或者方法會產生不符合正常邏輯的結果,則不是線程安全的。縱觀jdk的版本更新,能夠看到jdk的開發人員在高併發和多線程下了很大的功夫,儘量的經過jdk原生API來給開發人員帶來最方便最輕鬆的高併發數據模型,甚至想徹底爲開發人員解決併發問題,能夠看得出來jdk的開發人員確實很用心。可是在大量業務數據的邏輯代碼的狀況下高併發仍是不可避免,也不可能徹底經過jdk原生的併發API去解決這些併發問題,開發人員不得不本身去空值在高併發環境下的數據高可用性和一致性。node

前面說了jdk原生的API已經有了不少的高併發產品,在java.util.concurrent包下有不少解決高併發,高吞吐量,多線程問題的API。好比線程池ThreadPoolExecutor,線程池工廠Executors,Future模式下的接口Future,阻塞隊列BlockingQueue等等。c++

2、正文

一、數據的可見性

直接進入正題,concurrentHashMap相信用的人也不少,由於在數據安全性上確實比HashMap好用,在性能上比hashtable也好用。你們都知道線程在操做一個變量的時候,好比i++,jvm執行的時候須要通過兩個內存,主內存和工做內存。那麼在線程A對i進行加1的時候,它須要去主內存拿到變量值,這個時候工做內存中便有了一個變量數據的副本,執行完這些以後,再去對變量真正的加1,可是此時線程B也要操做變量,而且邏輯上也是沒有維護多線程訪問的限制,則頗有可能在線程A在從主內存獲取數據並在修改的時候線程B去主內存拿數據,可是這個時候主內存的數據尚未更新,A線程尚未來得及講加1後的變量回填到主內存,這個時候變量在這兩個線程操做的狀況下就會發生邏輯錯誤。數組

二、原子性

原子性就是當某一個線程A修改i的值的時候,從取出i到將新的i的值寫給i之間線程B不能對i進行任何操做。也就是說保證某個線程對i的操做是原子性的,這樣就能夠避免數據髒讀。安全

三、volatile的做用

Volatile保證了數據在多線程之間的可見性,每一個線程在獲取volatile修飾的變量時候都回去主內存獲取,因此當線程A修改了被volatile修飾的數據後其餘線程看到的必定是修改事後最新的數據,也是由於volatile修飾的變量數據每次都要去主內存獲取,在性能上會有些犧牲。數據結構

四、措施

HashMap在多線程的場景下是不安全的,hashtable雖然是在數據表上加鎖,縱然數據安全了,可是性能方面確實不如HashMap。那麼來看看concurrentHashMap是如何解決這些問題的。多線程

concurrentHashMap由多個segment組成,每個segment都包含了一個HashEntry數組的hashtable, 每個segment包含了對本身的hashtable的操做,好比get,put,replace等操做(這些操做與HashMap邏輯都是同樣的,不一樣的是concurrentHashMap在執行這些操做的時候加入了重入鎖ReentrantLock),這些操做發生的時候,對本身的hashtable進行鎖定。因爲每個segment寫操做只鎖定本身的hashtable,因此可能存在多個線程同時寫的狀況,性能無疑好於只有一個hashtable鎖定的狀況。通俗的講就是concurrentHashMap由多個hashtable組成。併發

五、源碼

看下concurrentHashMap的remove操做jvm

V remove(Object key, int hash, Object value) { lock();//重入鎖
            try { int c = count - 1; HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue = null; if (e != null) { V v = e.value; if (value == null || value.equals(v)) { oldValue = v; // All entries following removed node can stay // in list, but all preceding ones need to be // cloned.
                        ++modCount; HashEntry<K,V> newFirst = e.next; for (HashEntry<K,V> p = first; p != e; p = p.next) newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value); tab[index] = newFirst; count = c; // write-volatile
 } } return oldValue; } finally { unlock();//釋放鎖
 } }

Count是被volatile所修飾,保證了count的可見性,避免操做數據的時候產生邏輯錯誤。segment中的remove操做和HashMap大體同樣,HashMap沒有lock()和unlock()操做。高併發

看下concurrentHashMap的get源碼

V get(Object key, int hash) { if (count != 0) { // read-volatile
                HashEntry<K,V> e = getFirst(hash);         //若是沒有找到則直接返回null
                while (e != null) { if (e.hash == hash && key.equals(e.key)) {             //因爲沒有加鎖,在get的過程當中,可能會有更新,拿到的key對應的value可能爲null,須要單獨判斷一遍
                        V v = e.value;             //若是value爲不爲null,則返回獲取到的value
                        if (v != null) return v; return readValueUnderLock(e); // recheck
 } e = e.next; } } return null; } 

關於concurrentHashMap的get的相關說明已經在上面代碼中給出了註釋,這裏就很少說了。

看下concurrentHashMap中的put

public V put(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); return segmentFor(hash).put(key, hash, value, false); }

能夠看到concurrentHashMap不容許key或者value爲null

接下來看下segment的put

V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; if (c++ > threshold) // ensure capacity
 rehash(); HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue; if (e != null) { oldValue = e.value; if (!onlyIfAbsent) e.value = value; } else { oldValue = null; ++modCount; tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; // write-volatile
 } return oldValue; } finally { unlock(); } }

 一樣也是加入了重入鎖,其餘的基本和HashMap邏輯差很少。值得一提的是jdk8中添加的中的putval,這裏就很少說了。

3、總結

ConcurrentHashmap將數據結構分爲了多個Segment,也是使用重入鎖來解決高併發,講他分爲多個segment是爲了減少鎖的力度,添加的時候加了鎖,索引的時候沒有加鎖,使用volatile修飾count是爲了保持count的可見性,都是jdk爲了解決併發和多線程操做的經常使用手段。

相關文章
相關標籤/搜索