HashMap是面试题中的常见话题,我在面试中也讲过。没想到在开发的过程中,老驱动也被加载到了这里。具体场景是这样的。有一个列表查询数据,需要从其他接口查询某个字段的状态。为了优化查询速度,使用CompletableFuture.runAsync()并行调用接口查询,将查询结果放在一个HashMap中,以备后用。大致简化的代码如下:privatestaticvoidtask(){MapcontractsMap=Maps.newHashMap();Listfutures=Lists.newArrayList();Lists.newArrayList(123L,456L).forEach(uid->{futures.add(CompletableFuture.runAsync(()->{Booleaneffect=true;contractsMap.put(uid,effect);}));});CompletableFuture.allOf(futures.toArray(newCompletableFuture[]{})).join();System.out.println(contractsMap);}多次刷新查询列表,你会发现每次状态值都不一样。查询后打断点,发现contractsMap有时大小不一,莫名其妙丢失了部分数据。上面的简化任务方法运行几次后很容易重现这个现象。publicstaticvoidmain(String[]args){for(inti=0;i<100;i++){task();}}输出:{456=false,123=true}{456=false,123=true}{456=false,123=true}{456=false,123=true}{123=true}{456=false,123=true}{456=false,123=true}{123=true}{456=false,123=true}{456=false,123=true}{456=false,123=true}确认逻辑后还好,突然想起来这个HashMap是在多线程环境下使用的。出现。在网上搜索了一下,很多文章都提到了JDK8中HashMap可能存在的数据丢失问题,但基本上都是在说hash碰撞的情况。但是在上面的例子中,实际上有两个键,哈希值不一样,所以发生哈希冲突时是不可能丢失数据的。来看一下HashMap中的putVal方法:finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){Node[]tab;节点p;诠释n,我;如果((tab=table)==null||(n=tab.length)==0)n=(tab=resize()).length;如果((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);else{节点e;Kk;if(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k))))e=p;elseif(pinstanceofTreeNode)e=((TreeNode)p).putTreeVal(this,tab,hash,key,value);else{for(intbinCount=0;;++binCount){if((e=p.next)==null){p.next=newNode(hash,key,value,null);if(binCount>=TREEIFY_THRESHOLD-1)//-1f或第一个treeifyBin(tab,hash);休息;}if(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))))break;p=e;}}if(e!=null){//键VoldValue=e.value的现有映射;如果(!onlyIfAbsent||oldValue==null)e.value=value;节点访问后(e);返回旧值;}}++模数;如果(++大小>阈值)调整大小();节点插入后(逐出);返回空值;}一共有三个分支,第一个是第一次使用HashMap时tab为空,调用resize构造tab;第二个分支是构造一个节点,在没有hash冲突的情况下直接放入tab的slot中;第三个复杂分支用于处理哈希冲突。对于上面的例子,没有哈希冲突,所以数据丢失只能发生在第一和第二个分支的处理过程中。我个人的猜测可能是第一个线程进入putVal方法的时候,发现tab是空的,所以准备了resize方法;但是在构造tab之前,切换到第二个线程执行,同样发现tab是空的,于是进入resize方法并构造tab;然后进入第二个分支,将键值对放入选项卡中。切换回第一个线程,构造一个tab并赋值,然后在第二步覆盖tab;然后进入第二个分支,将键值对放入选项卡中。为了验证猜想,需要明确resize方法和newNode方法的相对执行顺序。但是并发条件不容易打断,所以我选择用JavaAgent的方式修改resize和newNode方法,在进入方法前后打印日志。publicclassJvmAgentDemo{publicstaticvoidpremain(Stringargs,Instrumentationinstrumentation){instrumentation.addTransformer(newTestTransformer(),true);试试{instrumentation.retransformClasses(Test.class);instrumentation.retransformClasses(HashMap.class);System.out.println("代理加载完成");}catch(Exceptione){System.out.println("代理加载失败");}}}publicclassTestTransformerimplementsClassFileTransformer{@Overridepublicbyte[]transform(ClassLoaderloader,StringclassName,Class>classBeingRedefined,ProtectionDomainprotectionDomain,byte[]classfileBuffer)throwsIllegalClassFormatException{System.out.println("转换"+班级名称);if("java/util/HashMap".equals(className)){尝试{ClassPoolcp=ClassPool.getDefault();CtClasscc=cp.get("java.util.HashMap");CtMethodm=cc.getDeclaredMethod("resize");m.insertBefore("{System.out.println(Thread.currentThread().getName()+\":resizestart\");}");m.insertAfter("{System.out.println(Thread.currentThread().getName()+\":resizeend\");;}");CtMethodm2=cc.getDeclaredMethod("newNode");m2.insertBefore("{System.out.println(Thread.currentThread().getName()+\":newNodestart\");}");m2.insertAfter("{System.out.println(Thread.currentThread().getName()+\":newNodeend\");}");returncc.toBytecode();}catch(Exceptione){e.printStackTrace();}}returnnull;}}willthe上面将agent打包成jar,在运行前面的taskcase时加载了agent,观察控制台的输出:ForkJoinPool.commonPool-worker-3:resizestartForkJoinPool.commonPool-worker-3:resizeendForkJoinPool。commonPool-worker-3:newNodestartForkJoinPool.commonPool-worker-2:newNodestartForkJoinPool.commonPool-worker-3:newNodeendForkJoinPool.commonPool-worker-2:newNodeend{456=false,123=true}ForkJoinPool.commonPool-worker-3:resizestartForkJoinPool.commonPool-worker-3:调整大小endForkJoinPool.commonPool-worker-3:新节点startForkJoinPool.commonPool-worker-3:新节点endForkJoinPool.commonPool-worker-2:调整大小startForkJoinPool.commonPool-worker-2:调整大小endForkJoinPool.commonPool-worker-2:新节点startForkJoinPool.commonPool-worker-2:newNodeend{456=false,123=true}ForkJoinPool.commonPool-worker-2:调整大小startForkJoinPool.commonPool-worker-3:调整大小startForkJoinPool.commonPool-worker-3:调整大小endForkJoinPool.commonPool-worker-3:newNodestartForkJoinPool.commonPool-worker-3:newNodeendForkJoinPool.commonPool-worker-2:resizeendForkJoinPool.commonPool-worker-2:newNodestartForkJoinPool.commonPool-worker-2:newNodeend{123=true}取出了三次运行时的日志,第一次和第二次都是正常情况,resize和newNode的执行顺序没有问题,无非就是第二次resize,但是resize内部创建新tab的时候会复制旧tab的内容,所以最后的结果也是正常的,但是第三次??的执行顺序时间和之前的猜测是一致的,worker-2线程进入resize方法后,返回前,worker-3线程也进入了resize方法。resize返回后,进入newNode方法创建节点;之后,worker-2线程继续执行,覆盖了3个线程写入的worker-Data。还有一些执行顺序也会造成数据丢失,但是我不太明白可能的执行顺序,比如:ForkJoinPool.commonPool-worker-1:resizestartForkJoinPool.commonPool-worker-2:resizestartForkJoinPool.commonPool-worker-2:调整大小endForkJoinPool.commonPool-worker-1:调整大小endForkJoinPool.commonPool-worker-1:新节点startForkJoinPool.commonPool-worker-1:新节点endForkJoinPool.commonPool-worker-2:新节点startForkJoinPool.commonPool-worker-2:新节点end{456=false}以上就是整个分析过程。虽然大家都知道HashMap不是线程安全的,但是在开发过程中,在多线程环境下使用它还是很容易的。看了很多,但离最终申请还有点距离。