使用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,在构造方法中修改。

时间: 2024-12-22 23:12:59

使用hashMap实现并发的相关文章

HashMap多线程并发问题分析

原文出处: 陶邦仁 并发问题的症状 多线程put后可能导致get死循环 从前我们的Java代码因为一些原因使用了HashMap这个东西,但是当时的程序是单线程的,一切都没有问题.后来,我们的程序性能有问题,所以需要变成多线程的,于是,变成多线程后到了线上,发现程序经常占了100%的CPU,查看堆栈,你会发现程序都Hang在了HashMap.get()这个方法上了,重启程序后问题消失.但是过段时间又会来.而且,这个问题在测试环境里可能很难重现. 我们简单的看一下我们自己的代码,我们就知道HashM

java--HashMap多线程并发问题分析

并发问题的症状 多线程put后可能导致get死循环 从前我们的Java代码因为一些原因使用了HashMap这个东西,但是当时的程序是单线程的,一切都没有问题.后来,我们的程序性能有问题,所以需要变成多线程的,于是,变成多线程后到了线上,发现程序经常占了100%的CPU,查看堆栈,你会发现程序都Hang在了HashMap.get()这个方法上了,重启程序后问题消失.但是过段时间又会来.而且,这个问题在测试环境里可能很难重现. 我们简单的看一下我们自己的代码,我们就知道HashMap被多个线程操作.

HashMap并发出现死循环 及 减少锁的竞争

线程不安全的HashMap, HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,查找时会陷入死循环. https://www.cnblogs.com/dongguacai/p/5599100.html https://coolshell.cn/articles/9606.html 减少锁的竞争3种方法: (1)减少锁的持有时间(缩小锁的范围) (2)降低锁的请求频率(降低锁的粒度) (3)放弃使用独占锁,使用并发容器,原子变量,读

HashMap,Hashtable,ConcurrentHashMap 和 synchronized Map 的原理和区别

HashMap 是否是线程安全的,如何在线程安全的前提下使用 HashMap,其实也就是HashMap,Hashtable,ConcurrentHashMap 和 synchronized Map 的原理和区别.当时有些紧张只是简单说了下HashMap不是线程安全的:Hashtable 线程安全,但效率低,因为是 Hashtable 是使用 synchronized 的,所有线程竞争同一把锁:而 ConcurrentHashMap 不仅线程安全而且效率高,因为它包含一个 segment 数组,将

Java多线程同步集合--并发库高级应用

ArrayBlockingQueueLinkedBlockingQueue 传统方式下用Collections工具类提供的synchronizedCollection方法来获得同步集合. java5中还提供了如下一些同步集合类:> java.util.concurrent - Java并发工具包> ConcurrentHashMap 进行HashMap的并发操作,用来替代Collections.synchronizedMap(m)方法.> ConcurrentSkipListMap 实现

关于Java并发编程的总结和思考

编写优质的并发代码是一件难度极高的事情.Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更好的选择.本文是对并发编程的一点总结和思考,同时也分享了Java5以后的版本中如何编写并发代码的一点点经验. 为什么需要并发 ??并发其实是一种解耦合的策略,它帮助我们把做什么(目标)和什么时候做(时机)分开.这样做可以明显改进应用程序的吞吐量(获得更多的CPU调度时间)和结构(程序有多个部分在协同

[Java集合] 彻底搞懂HashMap,HashTable,ConcurrentHashMap之关联.

注: 今天看到的一篇讲hashMap,hashTable,concurrentHashMap很透彻的一篇文章, 感谢原作者的分享. 原文地址: http://blog.csdn.net/zhangerqing/article/details/8193118 Java集合类是个非常重要的知识点,HashMap.HashTable.ConcurrentHashMap等算是集合类中的重点,可谓"重中之重",首先来看个问题,如面试官问你:HashMap和HashTable有什么区别,一个比较简

深入浅出 Java Concurrency (39): 并发总结 part 3 常见的并发陷阱

常见的并发陷阱 volatile volatile只能强调数据的可见性,并不能保证原子操作和线程安全,因此volatile不是万能的.参考指令重排序 volatile最常见于下面两种场景. a. 循环检测机制 volatile boolean done = false; while( ! done ){        dosomething();    } b. 单例模型 (http://www.blogjava.net/xylz/archive/2009/12/18/306622.html)

特约稿件 Java并发教程(Oracle官方资料)

本文是Oracle官方的Java并发相关的教程,感谢并发编程网的翻译和投递. (关注ITeye官微,随时随地查看最新开发资讯.技术文章.) 计算机的使用者一直以为他们的计算机可以同时做很多事情.他们认为当其他的应用程序在下载文件,管理打印队列或者缓冲音频的时候他们可以继续在文字处理程序上工作.甚至对于单个应用程序,他们任然期待它能在在同一时间做很多事情.举个例子,一个流媒体播放程序必须能同时完成以下工作:从网络上读取数字音频,解压缩数字音频,管理播放和更新程序显示.甚至文字处理器也应该能在忙于重