码迷,mamicode.com
首页 > 其他好文 > 详细

使用hashMap实现并发

时间:2016-07-22 21:34:34      阅读:207      评论:0      收藏:0      [点我收藏+]

标签:

 

  承认有些标题党味道,但却在实际异步框架中使用了。

比起“公认”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,在构造方法中修改。

 

使用hashMap实现并发

标签:

原文地址:http://www.cnblogs.com/31693884O/p/5695202.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!