承認有些標題黨味道,但卻在實際異步框架中使用了。
比起“公認”concurrentHashMap方式,提高有3-4倍的性能以及更低cpu占有率
需求
異步框架需要一個buffer,存放請求數據,多線程共享。
顯然這是一個多線程並發問題。
同步鎖方案
開始小覷了問題,以為只是簡單地鎖住資源、插入請求對象,都是內存操作,時間短,即使“堵”也不嚴重。

private void multiThreadSyncLock(final int numofThread,final Map<String,String> map) throws Exception { final long[] errCount=new long[numofThread+1]; Thread t = new Thread(new Runnable() { public void run() { for (int i = 0; i < numofThread; i++) { new Thread(new Runnable() { public void run() { String val=UUID.randomUUID().toString(); String key=Thread.currentThread().getName(); int index=Integer.parseInt(key.substring(7, key.length()))+1; long t1=System.currentTimeMillis(); for(int j=0;j<10000;j++) { synchronized(map) {map.put(key,val);} //獲得鎖后插入 if(!(val).equals(map.get(key))) errCount[0]++; //errCount >1 表示讀出數據和寫入不同 } long t2=System.currentTimeMillis(); errCount[index]=+errCount[index]+t2-t1; } }, "Thread-" + i).start(); } } }, "Yhread-main"); t.start(); Thread.currentThread().sleep(1000); t.join(); long tt=0; for(int i=1;i<=numofThread;i++) tt=tt+errCount[i]; log.debug("numofThread={},10,000 per thread,total time spent={}",numofThread,tt); Assert.assertEquals(0,errCount[0]); }
結果慘不忍睹!而且隨着並發線程數量增加,“堵”得嚴重
100並發,每線程申請插 入數據10000次,總耗時 |
200並發,每線程申請插 入數據10000次,總耗時 |
4567.3ms | 20423.95ms |
自旋鎖

@Test public void multiThreadPutConcurrentHashMap100() throws Exception{ final Map<String,String> map1=new ConcurrentHashMap<String,String>(512); for(int i=0;i<100;i++) multiThreadPutMap(100,map1); } private void multiThreadPutMap(final int numofThread,final Map<String,String> map) throws Exception { final long[] errCount=new long[numofThread+1]; Thread t = new Thread(new Runnable() { public void run() { for (int i = 0; i < numofThread; i++) { new Thread(new Runnable() { public void run() { String val=UUID.randomUUID().toString(); String key=Thread.currentThread().getName(); int index=Integer.parseInt(key.substring(7, key.length()))+1; long t1=System.currentTimeMillis(); for(int j=0;j<10000;j++) { map.put(key,val); //map的實現concurrentHashMap和HashMap if(!(val).equals(map.get(key))) errCount[0]++; //errCount >1 表示讀出數據和寫入不同 } long t2=System.currentTimeMillis(); errCount[index]=+errCount[index]+t2-t1; } }, "Thread-" + i).start(); } } }, "Yhread-main"); t.start(); Thread.currentThread().sleep(1000); t.join(); long tt=0; for(int i=1;i<=numofThread;i++) tt=tt+errCount[i]; log.debug("numofThread={},10,000 per thread,total time spent={}",numofThread,tt); Assert.assertEquals(0,errCount[0]); }
使用concurrentHashMap 100並發,每線程申請插入 數據10000次,耗時 |
使用concurrentHashMap 200並發,每線程申請插入 數據10000次,耗時 |
使用concurrentHashMap 300並發,每線程申請插入 數據10000次,耗時 |
200.69ms | 402.36ms | 542.08ms |
對比同步鎖,效率提高很多,約22-50倍,一個數量級的差距!
自旋鎖,線程一直在跑,避免了堵塞、喚醒來回切換的開銷,而且臨界狀態一條指令完成,大大提高了效率。
還能進一步優化?
眾所周知,hashmap數據結構是數組+鏈表(參考網上hashMap源碼分析)。
簡單來說,每次插入數據時:
- 會把給定的key轉換為數組指針p
- 如果array[p]為空,將vlaue保存到array[p]=value中,完成插入
- 如果array[p]不為空,建立一鏈表,插入兩個對象,並鏈表保存到array[p]中。
- 當填充率到默認的0.75,引起擴容。
當然有人會問
-
- 線程名字是唯一的?
- 怎么保證不同線程名hash后,對應不同指針?
- 線程會插入多個數據嗎?
- hashmap發生擴容怎么辦?
- 性能有改進嗎?
在回答前,先看看tomcat如何處理請求:請求達到后,tomcat會從線程池中,取出空閑線程,執行filter,最后servlet。
tomcat處理請求有以下特點:
-
- 處理線程,在返回結果前,不能處理新的請求
- 處理線程池通常不大,200-300左右。(現在,更傾向於使用多個“小”規模tomcat實例)
- 線程名字唯一
所以,
問題1、3 顯然是OK的
問題4,初始化hasmap時,預先分配一個較大的空間,可以避免擴容。比如對於300並發,new HashMap(512)。
請求雖然多,但是只有300個線程。
問題2,java對hash優化過,保證了hash的均勻度,避免重復。
public void checkHashcodeSpreadoutEnough() { int length=512; for(int j=0;j<10000;j++) { //重復1000次, Map<String,Object> map=new HashMap<String,Object>(length); for(int i=0;i<300;i++) { String key="Thread-"+(i+1); int hashcode=hash(key,length); Integer keyhashcode=new Integer(hashcode); log.debug("key={} hashcode={}",key,hashcode); if(map.containsKey(keyhashcode)) { //生成的hash值作為鍵保存在hash表,只要重復表示 有沖突 log.error("encounter collisions! key={} hashcode={}",key,hashcode); Assert.assertTrue("encounter collisions!", false); } } } } /* * 從hashMap源碼提取的hash值計算方法 * 跟蹤代碼得知,一般情況下沒有使用sun.misc.Hashing.stringHash32((String) k)計算 * 關於stringHash32,網上有評論,有興趣可以查查。 */ private int hash(Object k,int length) { int h = 0; h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); h=h ^ (h >>> 7) ^ (h >>> 4); return h & (length-1); }

publiwuwu void multiThreadPutHashMap100() throws Exception{ final Map<String,String> map=new HashMap<String,String>(512); //更換map實現為hashMap for(int i=0;i<100;i++) multiThreadPutMap(100,map); } // multiThreadPutMap(100,map); 參見concurrentHashMap單元測試代碼
使用HashMap 100並發,每線程申請插入 數據10000次,耗時 |
使用HashMap 200並發,每線程申請插入 數據10000次,耗時 |
使用HashMap 300並發,每線程申請插入 數據10000次,耗時 |
46.79ms | 99.42ms | 137.03 |
提高4.289164351倍 | 提高4.047073024倍 | 提高3.955922061倍 |
有近3-4倍的提升
結論
- 在一定的情況下,hashMap可以用在多線程並發環境下
- 同步鎖,屬於sleep-waiting類型的鎖。狀態發生變化,會引起cpu來回切換。因而效率低下。
- 自旋鎖,一直占有cpu,不停嘗試,直到成功。有時單核情況下,造成“假死"
- 仔細揣摩,分析,可以找到無“鎖”方式來解決多線程並發問題,能獲得更高性能和更小開銷
另,
在i5-2.5G,8G win10 jdk1.7.0_17,64bit
測試數據, 沒有考慮垃圾回收,數據有些波動。
hashMap默認填充因子為0.75,在構造方法中修改。