淺析ConcurrentHashMap


一、導論

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

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

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

二、正文

1、數據的可見性

直接進入正題,concurrentHashMap相信用的人也很多,因為在數據安全性上確實比HashMap好用,在性能上比hashtable也好用。大家都知道線程在操作一個變量的時候,比如i++,jvm執行的時候需要經過兩個內存,主內存和工作內存。那么在線程A對i進行加1的時候,它需要去主內存拿到變量值,這個時候工作內存中便有了一個變量數據的副本,執行完這些之后,再去對變量真正的加1,但是此時線程B也要操作變量,並且邏輯上也是沒有維護多線程訪問的限制,則很有可能在線程A在從主內存獲取數據並在修改的時候線程B去主內存拿數據,但是這個時候主內存的數據還沒有更新,A線程還沒有來得及講加1后的變量回填到主內存,這個時候變量在這兩個線程操作的情況下就會發生邏輯錯誤。

2、原子性

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

3、volatile的作用

Volatile保證了數據在多線程之間的可見性,每個線程在獲取volatile修飾的變量時候都回去主內存獲取,所以當線程A修改了被volatile修飾的數據后其他線程看到的一定是修改過后最新的數據,也是因為volatile修飾的變量數據每次都要去主內存獲取,在性能上會有些犧牲。

4、措施

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

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

5、源碼

看下concurrentHashMap的remove操作

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,這里就不多說了。

三、總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM