將大批量數據保存到map中有兩個地方的消耗將會是比較大的:第一個是擴容操作,第二個是鎖資源的爭奪。第一個擴容的問題,主要還是要通過配置合理的容量大小和擴容因子,盡可能減少擴容事件的發生;第二個鎖資源的爭奪,在put方法中會使用synchonized對頭節點進行加鎖,而鎖本身也是分等級的,因此我們的主要思路就是盡可能的避免鎖等級。所以,針對第二點,我們可以將數據通過通過ConcurrentHashMap的spread方法進行預處理,這樣我們可以將存在hash沖突的數據放在一個組里面,每個組都使用單線程進行put操作,這樣的話可以保證鎖僅停留在偏向鎖這個級別,不會升級,從而提升效率。
其他:
ConcurrentHashMap 1.7和1.8的區別
1、整體結構
1.7:Segment + HashEntry + Unsafe
1.8: 移除Segment,使鎖的粒度更小,Synchronized + CAS + Node + Unsafe
2、put()
1.7:先定位Segment,再定位桶,put全程加鎖,沒有獲取鎖的線程提前找桶的位置,並最多自旋64次獲取鎖,超過則掛起。
1.8:由於移除了Segment,類似HashMap,可以直接定位到桶,拿到first節點后進行判斷,1、為空則CAS插入;2、為-1則說明在擴容,則跟着一起擴容;3、else則加鎖put(類似1.7)
3、get()
基本類似,由於value聲明為volatile,保證了修改的可見性,因此不需要加鎖。
4、resize()
1.7:跟HashMap步驟一樣,只不過是搬到單線程中執行,避免了HashMap在1.7中擴容時死循環的問題,保證線程安全。
1.8:支持並發擴容,HashMap擴容在1.8中由頭插改為尾插(為了避免死循環問題),ConcurrentHashmap也是,遷移也是從尾部開始,擴容前在桶的頭部放置一個hash值為-1的節點,這樣別的線程訪問時就能判斷是否該桶已經被其他線程處理過了。
5、size()
1.7:很經典的思路:計算兩次,如果不變則返回計算結果,若不一致,則鎖住所有的Segment求和。
1.8:用baseCount來存儲當前的節點個數,這就設計到baseCount並發環境下修改的問題(說實話我沒看懂-_-!)。
JDK1.7中擴容中轉移數組transfer()方法源碼。
void transfer(Entry[] newTable, boolean rehash) { //新table的容量 int newCapacity = newTable.length; //遍歷原table for (Entry<K,V> e : table) { while(null != e) { //保存下一次循環的 Entry<K,V> Entry<K,V> next = e.next; if (rehash) { //通過e的key值計算e的hash值 e.hash = null == e.key ? 0 : hash(e.key); } //得到e在新table中的插入位置 int i = indexFor(e.hash, newCapacity); //采用鏈頭插入法將e插入i位置,最后得到的鏈表相對於原table正好是頭尾相反的 e.next = newTable[i]; newTable[i] = e; //下一次循環 e = next; } } }
原文鏈接:https://blog.csdn.net/u013374645/article/details/88700927