使用hashMap實現並發


 

  承認有些標題黨味道,但卻在實際異步框架中使用了。

比起“公認”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源碼分析)。
        簡單來說,每次插入數據時:

  1.  會把給定的key轉換為數組指針p
  2.  如果array[p]為空,將vlaue保存到array[p]=value中,完成插入
  3.  如果array[p]不為空,建立一鏈表,插入兩個對象,並鏈表保存到array[p]中。
  4.  當填充率到默認的0.75,引起擴容。
    javadoc中明確指出hashMap非線程安全。
 
  但注意准確地說,不安全在於:
        1、key重復,鏈表插入。
        2、擴容,填充率小於0.75
    在保證key不重復,應該是key的hash不重復,同時填充率小於0.75情況下,多線程插入/讀取時安全的。    
    所以,可以進一步優化,使用線程名字作為key,把請求數據,插入hashMap中。
    避免了鎖!
 

  當然有人會問

    1. 線程名字是唯一的?
    2. 怎么保證不同線程名hash后,對應不同指針?
    3. 線程會插入多個數據嗎?
    4. hashmap發生擴容怎么辦?
    5. 性能有改進嗎?

  在回答前,先看看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);
    }  
  上面的代碼,對300個線程名進行hash計算,檢測是否沖突,並重復運行10000次。
  結果表明沒有發生沖突。換句話hashMap的hash算法均勻度沒有問題,
  特別是在本案例環境中,能保證hash唯一!
  
  問題5,性能問題,看單元測試結果
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無鎖並發
使用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,在構造方法中修改。

 


免責聲明!

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



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