承认有些标题党味道,但却在实际异步框架中使用了。
比起“公认”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.3ms20423.95ms
对比同步锁,效率提高很多,约22-50倍,一个数量级的差距!
自旋锁,线程一直在跑,避免了堵塞、唤醒来回切换的开销,而且临界状态一条指令完成,大大提高了效率。
众所周知,hashmap数据结构是数组+链表(参考网上hashMap源码分析)。 简单来说,每次插入数据时:
会把给定的key转换为数组指针p 如果array[p]为空,将vlaue保存到array[p]=value中,完成插入 如果array[p]不为空,建立一链表,插入两个对象,并链表保存到array[p]中。 当填充率到默认的0.75,引起扩容。 javadoc中明确指出hashMap非线程安全。 但注意准确地说,不安全在于: 1、key重复,链表插入。 2、扩容,填充率小于0.75 在保证key不重复,应该是key的hash不重复,同时填充率小于0.75情况下,多线程插入/读取时安全的。 所以,可以进一步优化,使用线程名字作为key,把请求数据,插入hashMap中。 避免了锁!当然有人会问
线程名字是唯一的? 怎么保证不同线程名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); } 上面的代码,对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无锁并发 使用HashMap100并发,每线程申请插入数据10000次,耗时使用HashMap200并发,每线程申请插入数据10000次,耗时使用HashMap300并发,每线程申请插入数据10000次,耗时46.79ms99.42ms137.03提高4.289164351倍提高4.047073024倍提高3.955922061倍
有近3-4倍的提升
另,
在i5-2.5G,8G win10 jdk1.7.0_17,64bit
测试数据, 没有考虑垃圾回收,数据有些波动。
hashMap默认填充因子为0.75,在构造方法中修改。
转载于:https://www.cnblogs.com/31693884O/p/5695202.html
