前言列举了大家在工作中容易犯的几个并发错误。都是实际项目代码中看到的生动例子。希望对您有所帮助。第一滴血总是在线出现:ERROR1062(23000)Duplicateentry'xxx'forkey'yyy',我们来看看有问题的代码:UserBindInfoinfo=selectFromDB(userId);if(info==null){info=newUserBindInfo(userId,deviceId);insertIntoDB(info);}else{info.setDeviceId(deviceId);updateDB(info);}并发情况下,如果第一步判断为空,则有2个或多个线程进入插入数据库操作,此时多次插入同一个ID。正确处理姿势:insertintoUserBindInfovalues(#{userId},#{deviceId})onduplicatekeyupdatedeviceId=#{deviceId}多次,导致插入失败。一般来说,你可以使用insert...onduplicatekeyupdate...来解决这个问题。注意:如果UserBindInfo表有一个主键和多个唯一索引,在并发情况下,使用insert...onduplicatekey可能会造成死锁(Mysql5.7),可以这样处理:try{UserBindInfoMapper.insertIntoDB(userBindInfo);}catch(DuplicateKeyExceptionex){UserBindInfoMapper.update(userBindInfo);}DoubleKill小心你的全局变量,例如下面的代码:ss");publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadPoolExecutorthreadPoolExecutor=newThreadPoolExecutor(10,100,1,TimeUnit.MINUTES,newLinkedBlockingQueue<>(1000));while(true){threadPoolExecutor.execute(()->{StringdateString=sdf.format(newDate());try{DateparseDate=sdf.parse(dateString);StringdateString2=sdf.format(parseDate);System.out.println(dateString.equals(dateString2));}catch(ParseExceptione){e.printStackTrace();}});}}}可以看到SimpleDa向全局变量抛出异常的teFormat。在并发的情况下,存在安全问题。阿里Java规范明确要求慎用。除了SimpleDateFormat,其实很多时候,面对全局变量,我们需要考虑是否存在并发问题,如下@ComponentpublicclassTest{publicstaticListdesc=newArrayList<>();publicListgetDescByUserType(intuserType){if(userType==1){desc.add("普通会员不能收发邮件,请购买会员");returndesc;}elseif(userType==2){desc.add("恭喜您已经是VIP会员,享受发送邮件");returndesc;}else{desc.add("你的身份未知");returndesc;}}}因为desc是一个全局变量,在并发情况下,如果请求getDescByUserType方法,得到的可能不是你想要的结果。TribleKill假设有如下业务:控制同一个用户访问某个界面的频率不能小于5秒。一般很容易想到使用redis的setnx操作来控制并发访问,于是就有了如下代码:if(RedisOperation.setnx(userId,1)){RedisOperation.expire(userId,5,TimeUnit.SECONDS));//执行正常的业务逻辑}else{return"toofrequentaccess";}假设执行了setnx操作,在设置expireTime之前,机器重启或者突然死机,就会出现死锁。后面执行setnx的时候用户id永远为false,这可能会让你永远失去那个用户。那么如何解决这个问题,可以考虑使用SETkeyvalueNXEXmax-lock-time,这是Redis中实现锁的方法,是原子操作。不会像上面的代码那样分两步执行,先set然后expire,是一步。客户端执行以上命令:如果服务器返回OK,则客户端获取锁。如果服务器返回NIL,则客户端获取锁失败,可以稍后重试。达到设置的过期时间后,会自动释放锁QuadraKill我们来看一段关于ConcurrentHashMap的代码,如下://全局变量Mapmap=newConcurrentHashMap();Integervalue=count.get(k);if(value==null){map.put(k,1);}else{map.put(k,value+1);}假设两个线程都输入value==null,结果这一步会不会变小?OK,客官先休息一下,闭眼休息一会儿,我们来验证一下,看个demo:publicstaticvoidmain(String[]args){for(inti=0;i<1000;i++){testConcurrentMap();}}privatestaticvoidtestConcurrentMap(){finalMapcount=newConcurrentHashMap<>();ExecutorServiceexecutorService=Executors.newFixedThreadPool(2);finalCountDownLatchendLatch=newCountDownLatch(2);Runnabletask=()->{for(inti=0;i<5;i++){Integervalue=count.get("k");if(null==value){System.out.println(Thread.currentThread().getName());count.put("k",1);}else{count.put("k",value+1);}}endLatch.countDown();};executorService.execute(任务);executorService.execute(任务);try{endLatch.await();if(count.get("k")<10){System.out.println(count);}}catch(Exceptione){e.printStackTrace();}从表面上看,运行结果应该都是10吧?好,我们再看一下运行结果:运行结果中出现了5,所以这个实现存在并发问题。那么正确的实施姿势是怎样的呢?Mapmap=newConcurrentHashMap();Vv=map.get(k);if(v==null){Vv=newV();Vold=map.putIfAbsent(k,v);if(old!=null){v=old;}}可以考虑使用putIfAbsent来解决这个问题(1)如果key是一条新的记录,那么会将key-value对加入到map中,返回null(2)如果key已经存在,则不覆盖已有的值,返回已有的值。我们看一下下面的代码和运行结果:(2);finalMapmap=Maps.newConcurrentMap();finalCountDownLatchcountDownLatch=newCountDownLatch(2);Runnabletask=()->{AtomicIntegeroldValue;for(inti=0;i<5;i++){oldValue=map.get("k");if(null==oldValue){AtomicIntegerinitValue=newAtomicInteger(0);oldValue=map.putIfAbsent("k",initValue);if(oldValue==null){oldValue=initValue;}}oldValue.incrementAndGet();}countDownLatch.countDown();};executorService.execute(任务);executorService.execute(任务);try{countDownLatch.await();System.out.println(map);}catch(Exceptione){e.printStackTrace();}}五杀目前有以下业务场景:用户手头有代金券,可以兑换相应的现金,错误演示1if(isAvailable(ticketId){1,添加现金操作2,deleteTicketById(ticketId)}else{return"nocashcouponavailable"}分析:假设有两个线程A和B要兑换现金,执行顺序如下:1.线程A添加现金2.线程B添加现金3.线程A删除ticketmark4.线程B删除ticketmark显然,有这个是有问题的,已经给用户加了两次现金。错误演示2if(isAvailable(ticketId){1,deleteTicketById(ticketId)2,add操作到cash}else{return"Nocashcouponsavailable"}并发,如果是线程,第一步deleteTicketById删除失败,更多cash会正确解决方法if(deleteAvailableTicketById(ticketId)==1){1,添加现金操作}else{return"nocashcouponsavailable"}