並發編程-concurrent指南-ConcurrentMap
ConcurrentMap 是個接口,你想要使用它的話就得使用它的實現類之一。
ConcurrentMap,它是一個接口,是一個能夠支持並發訪問的java.util.map集合;
在原有java.util.map接口基礎上又新提供了4種方法,進一步擴展了原有Map的功能:
public interface ConcurrentMap<K, V> extends Map<K, V> { //插入元素 V putIfAbsent(K key, V value); //移除元素 boolean remove(Object key, Object value);//替換元素 boolean replace(K key, V oldValue, V newValue); //替換元素 V replace(K key, V value); }
putIfAbsent:與原有put方法不同的是,putIfAbsent方法中如果插入的key相同,則不替換原有的value值;
remove:與原有remove方法不同的是,新remove方法中增加了對value的判斷,如果要刪除的key--value不能與Map中原有的key--value對應上,則不會刪除該元素;
replace(K,V,V):增加了對value值的判斷,如果key--oldValue能與Map中原有的key--value對應上,才進行替換操作;
replace(K,V):與上面的replace不同的是,此replace不會對Map中原有的key--value進行比較,如果key存在則直接替換;
其實,對於ConcurrentMap來說,我們更關註Map本身的操作,在並發情況下是如何實現數據安全的。在java.util.concurrent包中,ConcurrentMap的實現類主要以ConcurrentHashMap為主。接下來,我們具體來看下。
1.2 ConcurrentHashMap
ConcurrentHashMap是一個線程安全,並且是一個高效的HashMap。
但是,如果從線程安全的角度來說,HashTable已經是一個線程安全的HashMap,那推出ConcurrentHashMap的意義又是什麽呢?
說起ConcurrentHashMap,就不得不先提及下HashMap在線程不安全的表現,以及HashTable的效率!
HashMap
關於HashMap的講解,可以參考 HashMap 的底層原理 和 Java集合,HashMap底層實現和原理(1.7數組+鏈表與1.8+的數組+鏈表+紅黑樹)以及 紅黑樹
在此節中,我們主要來說下,在多線程情況下HashMap的表現?
HashMap中添加元素的源碼:(基於JDK1.7.0_45)
public V put(K key, V value) { 。。。忽略 addEntry(hash, key, value, i); return null; } void addEntry(int hash, K key, V value, int bucketIndex) { 。。。忽略 createEntry(hash, key, value, bucketIndex); } //向鏈表頭部插入元素:在數組的某一個角標下形成鏈表結構; void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
在多線程情況下,同時A、B兩個線程走到createEntry()方法中,並且這兩個線程中插入的元素hash值相同,bucketIndex值也相同,那麽無論A線程先執行,還是B線程先被執行,最終都會2個元素先後向鏈表的頭部插入,導致互相覆蓋,致使其中1個線程中的數據丟失。這樣就造成了HashMap的線程不安全,數據的不一致;
更要命的是,HashMap在多線程情況下還會出現死循環的可能,造成CPU占用率升高,導致系統卡死。
舉個簡單的例子:public class ConcurrentHashMapTest { public static void main(String[] agrs) throws InterruptedException { final HashMap<String,String> map = new HashMap<String,String>(); Thread t = new Thread(new Runnable(){ public void run(){ for(int x=0;x<10000;x++){ Thread tt = new Thread(new Runnable(){ public void run(){ map.put(UUID.randomUUID().toString(),""); } }); tt.start(); System.out.println(tt.getName()); } } }); t.start(); t.join(); } }
在上面的例子中,我們利用for循環,啟動了10000個線程,每個線程都向共享變量中添加一個元素。
測試結果:通過使用JDK自帶的jconsole工具,可以看到HashMap內部形成了死循環,並且主要集中在兩處代碼上。
HashMap--put()494行:(基於JDK1.7.0_45)
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) {------**for循環494行** Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
HashMap--transfer()601行:(基於JDK1.7.0_45)
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; }-----**while循環601行** } }
通過查看代碼,可以看出,死循環的產生:主要因為在遍歷數組角標下的鏈表時,沒有了為null的元素,單向鏈表變成了循環鏈表,頭尾相連了。
以上兩點,就是HashMap在多線程情況下的表現。
- HashTable
說完了HashMap的線程不安全,接下來說下HashTable的效率!!
HashTable與HashMap的結構一致,都是哈希表實現。
與HashMap不同的是,在HashTable中,所有的方法都加上了synchronized鎖,用鎖來實現線程的安全性。由於synchronized鎖加在了HashTable的每一個方法上,所以這個鎖就是HashTable本身--this。那麽,可想而知HashTable的效率是如何,安全是保證了,但是效率卻損失了。
無論執行哪個方法,整個哈希表都會被鎖住,只有其中一個線程執行完畢,釋放所,下一個線程才會執行。無論你是調用get方法,還是put方法皆是如此;
說完了HashMap和HashTable,下面我們就重點介紹下ConcurrentHashMap,看看ConcurrentHashMap是如何來解決上述的兩個問題的!1.3 ConcurrentHashMap結構
在說到ConcurrentHashMap源碼之前,我們首先來了解下ConcurrentHashMap的整體結構,這樣有利於我們快速理解源碼。
不知道,大家還是否記得HashMap的整體結構呢?如果忘記的話,我們就在此進行回顧下!
HashMap底層使用數組和鏈表,實現哈希表結構。插入的元素通過散列的形式分布到數組的各個角標下;當有重復的散列值時,便將新增的元素插入在鏈表頭部,使其形成鏈表結構,依次向後排列。
下面是,ConcurrentHashMap的結構:
與HashMap不同的是,ConcurrentHashMap中多了一層數組結構,由Segment和HashEntry兩個數組組成。其中Segment起到了加鎖同步的作用,而HashEntry則起到了存儲K.V鍵值對的作用。
在ConcurrentHashMap中,每一個ConcurrentHashMap都包含了一個Segment數組,在Segment數組中每一個Segment對象則又包含了一個HashEntry數組,而在HashEntry數組中,每一個HashEntry對象保存K-V數據的同時又形成了鏈表結構,此時與HashMap結構相同。
在多線程中,每一個Segment對象守護了一個HashEntry數組,當對ConcurrentHashMap中的元素修改時,在獲取到對應的Segment數組角標後,都會對此Segment對象加鎖,之後再去操作後面的HashEntry元素,這樣每一個Segment對象下,都形成了一個小小的HashMap,在保證數據安全性的同時,又提高了同步的效率。只要不是操作同一個Segment對象的話,就不會出現線程等待的問題!
本文轉自:https://www.jianshu.com/p/8f7b2cd34c47
並發編程-concurrent指南-ConcurrentMap