ConcurrentHashMap是JDK 1.5推出的類,性能上比HashTable和Collections.synchronizedMap(new HashMap())快很多。
看此類源碼最好和HashTable對比理解,會發現它的優化,此類一出HashTable可廢。
優化的方向,降低讀對鎖的依賴,寫都是加鎖。
一,主要是用了分離鎖
1.概括結構如下,可以簡單理解成把一個大的HashTable分解成多個,形成了鎖分離。
ConcurrentHashMap默認是分離了16個模塊,即理想狀態下能有16個線程同時並發(指要修改的map處於不同的模塊之中)。
采用分離鎖可以避免無意義的等待,相比。在HashTable中實現就簡單多了,代碼如下
public synchronized V put(K key, V value) { ... }
HashTable是直接在方法前加了synchronized關鍵字,把整塊都鎖上了
2,應用場景
當有一個大數組時需要在多個線程共享時就可以考慮是否把它給分層多個節點了,避免大鎖。並可以考慮通過hash算法進行一些模塊定位。
其實不止用於線程,當設計數據表的事務時(事務某種意義上也是同步機制的體現),可以把一個表看成一個需要同步的數組,如果操作的表數據太多時就可以考慮事務分離了(這也是為什么要避免大表的出現),比如把數據進行字段拆分,水平分表等
二,和使用final變量
1, final Segment<K,V>[] segments;
static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; ... ... //全是final }
這樣構建列表主要特性是,並發時你讀到的值永遠不會改變. 所以讀的時候並不需要加鎖
V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry<K,V> e = getFirst(hash);//這里取的是table的head,因為table用的是volatile也不需要擔心並發remove時重排 while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; //當然這里value用的是volatile也是不加鎖直接可以的讀原因 if (v != null) return v; return readValueUnderLock(e); // recheck 當然這里是防止當修改時或者remove時的 } e = e.next; } } return null; }
這里用的Key和 next都是final根本不需要擔心鏈正在被修改,而鏈的頭部,即table[n]是volatile元素,也不需要擔心同步
當然增加了final,相應的方法也不對了,最好的就是對比下和HashTable的remove方法
2, 當有固定結構,如鏈表,樹等結構時可以嘗試把指針即(如next)和元素標識(如key ,id)做final處理,可以學下ConcurrentHashMap的remove方法,不過注意它的頭是同步的.
三,和volatile變量代替了線程的鎖。
在ConcurrentHashMap中,主要定義volatile變量有
transient volatile int count;
transient volatile HashEntry<K,V>[] table;
1. 概括.
在Java Memory Model中一般JVM會優化把變量寫入當前線程的高速緩存,然后在在先入共享內存中,而Volatile則要求去掉JVM的優化,直接把變量寫入JVM的共享內存中,性能上有很大的損耗,從而也失去了程序的原子性。
寫個代碼測試證明下,如下

package org.benson.another; /** * * @author BensonHe QQ 277803242 * @blogFrom http://www.cnblogs.com/springsource * TODO Research for volatile variable */ public class Test4Volatile { public volatile int volaCount = 0; public int nonVolaCount = 0; /** * run the same job for a volatile and non-volatile variable * @param loopCount * @return how much the job spend seconds */ public long howLongVolatile(long loopCount) { long startTime = java.util.Calendar.getInstance().getTime().getTime(); for (int i = 0; i < loopCount; i++) volaCount++; return java.util.Calendar.getInstance().getTime().getTime() - startTime; } /** * run the same job for non-volatile variable * @param loopCount * @return how much the job spend seconds */ public long howLongNonVolatile(long loopCount) { long startTime = java.util.Calendar.getInstance().getTime().getTime(); for (int i = 0; i < loopCount; i++) nonVolaCount++; return java.util.Calendar.getInstance().getTime().getTime() - startTime; } public static void main(String[] args) { Test4Volatile test4Volatile = new Test4Volatile(); System.out.println("the volatil variable spend "+test4Volatile.howLongVolatile(100000000l)); System.out.println("the non-volatil variable spend "+test4Volatile.howLongNonVolatile(100000000l)); } }
運行結果如下
the volatil variable spend 1860 the non-volatil variable spend 284
這就是JVM優化的能力
所以要正確的使用variable很重要,看看ConcurrentHashMap中的代碼對它的應用,以count為例
V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); //放進一個同步快中 try { int c = count; //先賦值給一個非volatile變量進行技術 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(); } }
所以,當要對改變量進行計算時,可以把變量賦值到另一個非volatile變量(當然要注意此時計算變量也失去了和共享內存中的一致性,但賦給了原子性了,所以必須放到同步塊中增加一致性),進行計算,待計算完成后再賦值給volatile變量,寫入共享內存。
2,應用場景推薦
讀的操作遠遠大於寫的時候可以用volatile優化,代替同步。
如果只讀的話,性能和非volatile都是一樣的,並保證一致性
修改代碼證明

package org.benson.another; /** * * @author BensonHe QQ 277803242 * @blogFrom http://www.cnblogs.com/springsource * TODO Research for volatile variable */ public class Test4Volatile { public volatile int volaCount = 0; public int nonVolaCount = 0; public int readCount; /** * run the same job for a volatile and non-volatile variable * @param loopCount * @return how much the job spend seconds */ public long howLongVolatile(long loopCount) { long startTime = java.util.Calendar.getInstance().getTime().getTime(); for (int i = 0; i < loopCount; i++) readCount=volaCount; return java.util.Calendar.getInstance().getTime().getTime() - startTime; } /** * run the same job for non-volatile variable * @param loopCount * @return how much the job spend seconds */ public long howLongNonVolatile(long loopCount) { long startTime = java.util.Calendar.getInstance().getTime().getTime(); for (int i = 0; i < loopCount; i++) readCount=nonVolaCount; return java.util.Calendar.getInstance().getTime().getTime() - startTime; } public static void main(String[] args) { Test4Volatile test4Volatile = new Test4Volatile(); System.out.println("the volatil variable spend "+test4Volatile.howLongVolatile(100000000l)); System.out.println("the non-volatil variable spend "+test4Volatile.howLongNonVolatile(100000000l)); } }
輸出
the volatil variable spend 286 the non-volatil variable spend 279
可以看到性能幾乎一樣
附:
Volatile 變量法則:對 Volatile 域的寫入操作 happens-before 於每個后續對同一 Volatile 的讀操作。
如果正確使用Volatile 可以優化線程,使用Volatile原則如下當對一個變量的讀遠遠大於對其修改操作時可以考慮用Volatile優化了。
歡迎交流, QQ 107966750
參考資料: