使用hashMap实现并发

it2022-05-05  127

 

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

比起“公认”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

 

 

 

自旋锁

@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]); }  自旋锁测试代码 使用concurrentHashMap100并发,每线程申请插入数据10000次,耗时使用concurrentHashMap200并发,每线程申请插入数据10000次,耗时使用concurrentHashMap300并发,每线程申请插入数据10000次,耗时200.69ms402.36ms542.08ms

 

 

 

 

对比同步锁,效率提高很多,约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倍的提升

结论

在一定的情况下,hashMap可以用在多线程并发环境下同步锁,属于sleep-waiting类型的锁。状态发生变化,会引起cpu来回切换。因而效率低下。自旋锁,一直占有cpu,不停尝试,直到成功。有时单核情况下,造成“假死"仔细揣摩,分析,可以找到无“锁”方式来解决多线程并发问题,能获得更高性能和更小开销

 

 

另,

   在i5-2.5G,8G win10 jdk1.7.0_17,64bit

   测试数据, 没有考虑垃圾回收,数据有些波动。

  hashMap默认填充因子为0.75,在构造方法中修改。

 

转载于:https://www.cnblogs.com/31693884O/p/5695202.html


最新回复(0)