日常工作中很容易犯的几个并发错误

 前言

列举大家平时在工作中很容易犯的几个并发错误,都是在实际项目代码中看到的鲜活例子,希望对大家有帮助。

First Blood

线上总是出现:ERROR 1062 (23000) Duplicate entry 'xxx' for key 'yyy',我们来看一下有问题的这段代码:

 
 
 
 
  1. UserBindInfo info = selectFromDB(userId); 
  2. if(info == null){ 
  3.     info = new UserBindInfo(userId,deviceId); 
  4.     insertIntoDB(info); 
  5. }else{ 
  6.     info.setDeviceId(deviceId); 
  7.     updateDB(info); 
  8.     } 

在并发情况下,第一步判断都为空,就会有2个或者多个线程进入插入数据库操作,这时候就出现了同一个ID插入多次。

正确处理姿势:

 
 
 
 
  1. insert into UserBindInfo values(#{userId},#{deviceId}) on duplicate key update deviceId=#{deviceId}多次的情况,导致插入失败。 

一般情况下,可以用insert...on duplicate key update... 解决这个问题。

注意: 如果UserBindInfo表存在主键以及一个以上的唯一索引,在并发情况下,使用insert...on duplicate key,可能会产生死锁(Mysql5.7),可以这样处理:

 
 
 
 
  1. try{ 
  2.    UserBindInfoMapper.insertIntoDB(userBindInfo); 
  3. }catch(DuplicateKeyException ex){ 
  4.     UserBindInfoMapper.update(userBindInfo); 

Double Kill

小心你的全局变量,如下面这段代码:

 
 
 
 
  1.  public class GlobalVariableConcurrentTest { 
  2.    
  3.       private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
  4.   
  5.       public static void main(String[] args) throws InterruptedException { 
  6.          ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000)); 
  7.   
  8.          while (true){ 
  9.              threadPoolExecutor.execute(()->{ 
  10.                 String dateString = sdf.format(new Date()); 
  11.                  try { 
  12.                      Date parseDate = sdf.parse(dateString); 
  13.                     String dateString2 = sdf.format(parseDate); 
  14.                      System.out.println(dateString.equals(dateString2)); 
  15.                  } catch (ParseException e) { 
  16.                      e.printStackTrace(); 
  17.                 } 
  18.              }); 
  19.         } 
  20.   
  21.      } 
  22.   

可以看到有异常抛出

全局变量的SimpleDateFormat,在并发情况下,存在安全性问题,阿里Java规约明确要求谨慎使用它。

除了SimpleDateFormat,其实很多时候,面对全局变量,我们都需要考虑并发情况是否存在问题,如下

 
 
 
 
  1.   @Component 
  2.   public class Test { 
  3.   
  4.      public static List desc = new ArrayList<>(); 
  5.    
  6.       public List getDescByUserType(int userType) { 
  7.           if (userType == 1) { 
  8.               desc.add("普通会员不可以发送和查看邮件,请购买会员"); 
  9.               return desc; 
  10.          } else if (userType == 2) { 
  11.              desc.add("恭喜你已经是VIP会员,尽情的发邮件吧"); 
  12.             return desc; 
  13.          }else { 
  14.              desc.add("你的身份未知"); 
  15.              return desc; 
  16.          } 
  17.      } 
  18.  } 

因为desc是全局变量,在并发情况下,请求getDescByUserType方法,得到的可能并不是你想要的结果。

Trible Kill

假设现在有如下业务:控制同一个用户访问某个接口的频率不能小于5秒。一般很容易想到使用redis的 setnx操作来控制并发访问,于是有以下代码:

 
 
 
 
  1.  if(RedisOperation.setnx(userId, 1)){ 
  2.      RedisOperation.expire(userId,5,TimeUnit.SECONDS)); 
  3.       //执行正常业务逻辑 
  4.  }else{ 
  5.       return “访问过于频繁”; 
  6.  } 

假设执行完setnx操作,还没来得及设置expireTime,机器重启或者突然崩溃,将会发生死锁。该用户id,后面执行setnx永远将为false,这可能让你永远损失那个用户。

那么怎么解决这个问题呢,可以考虑用SET key value NX EX max-lock-time ,它是一种在 Redis 中实现锁的方法,是原子性操作,不会像以上代码分两步执行,先set再expire,它是一步到位。

客户端执行以上的命令:

  • 如果服务器返回 OK ,那么这个客户端获得锁。
  • 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
  • 设置的过期时间到达之后,锁将自动释放

Quadra Kill

我们看一下有关ConcurrentHashMap的一段代码,如下:

 
 
 
 
  1.   //全局变量 
  2.   Map map = new ConcurrentHashMap();  
  3.   
  4.  Integer value = count.get(k); 
  5.  if(value == null){ 
  6.          map.put(k,1); 
  7.   }else{ 
  8.       map.put(k,value+1); 
  9.   } 

假设两条线程都进入 value==null,这一步,得出的结果是不是会变小?OK,客官先稍作休息,闭目养神一会,我们验证一下,请看一个demo:

 
 
 
 
  1.   public static void main(String[] args)  { 
  2.           for (int i = 0; i < 1000; i++) { 
  3.               testConcurrentMap(); 
  4.           } 
  5.       } 
  6.       private static void testConcurrentMap() { 
  7.           final Map count = new ConcurrentHashMap<>(); 
  8.           ExecutorService executorService = Executors.newFixedThreadPool(2); 
  9.          final CountDownLatch endLatch = new CountDownLatch(2); 
  10.          Runnable task = ()->  { 
  11.                  for (int i = 0; i < 5; i++) { 
  12.                      Integer value = count.get("k"); 
  13.                      if (null == value) { 
  14.                          System.out.println(Thread.currentThread().getName()); 
  15.                          count.put("k", 1); 
  16.                      } else { 
  17.                          count.put("k", value + 1); 
  18.                      } 
  19.                  } 
  20.                  endLatch.countDown(); 
  21.          }; 
  22.   
  23.          executorService.execute(task); 
  24.          executorService.execute(task); 
  25.  
  26.         try { 
  27.              endLatch.await(); 
  28.              if (count.get("k") < 10) { 
  29.                  System.out.println(count); 
  30.              } 
  31.         } catch (Exception e) { 
  32.             e.printStackTrace(); 
  33.         } 

表面看,运行结果应该都是10对吧,好的,我们再看运行结果:

运行结果出现了5,所以这样实现是有并发问题的,那么正确的实现姿势是啥呢?

 
 
 
 
  1.   Map map = new ConcurrentHashMap();  
  2.   V v = map.get(k); 
  3.   if(v == null){ 
  4.           V v = new V(); 
  5.           V old = map. putIfAbsent(k,v); 
  6.           if(old != null){ 
  7.                    v = old; 
  8.           } 
  9.   } 

可以考虑使用putIfAbsent解决这个问题

(1)如果key是新的记录,那么会向map中添加该键值对,并返回null。

(2)如果key已经存在,那么不会覆盖已有的值,返回已经存在的值

我们再来看看以下代码以及运行结果:

 
 
 
 
  1.   public static void main(String[] args)  { 
  2.           for (int i = 0; i < 1000; i++) { 
  3.               testConcurrentMap(); 
  4.           } 
  5.       } 
  6.    
  7.       private static void testConcurrentMap() { 
  8.           ExecutorService executorService = Executors.newFixedThreadPool(2); 
  9.           final Map map = Maps.newConcurrentMap(); 
  10.          final CountDownLatch countDownLatch = new CountDownLatch(2); 
  11.   
  12.          Runnable task = ()->  { 
  13.                  AtomicInteger oldValue; 
  14.                  for (int i = 0; i < 5; i++) { 
  15.                      oldValue = map.get("k"); 
  16.                     if (null == oldValue) { 
  17.                          AtomicInteger initValue = new AtomicInteger(0); 
  18.                          oldValue = map.putIfAbsent("k", initValue); 
  19.                          if (oldValue == null) { 
  20.                              oldValue = initValue; 
  21.                          } 
  22.                      } 
  23.                      oldValue.incrementAndGet(); 
  24.                  } 
  25.              countDownLatch.countDown(); 
  26.          }; 
  27.   
  28.          executorService.execute(task); 
  29.          executorService.execute(task); 
  30.   
  31.          try { 
  32.              countDownLatch.await(); 
  33.              System.out.println(map); 
  34.          } catch (Exception e) { 
  35.              e.printStackTrace(); 
  36.       } 
  37.     }

Penta Kill

现有如下业务场景:用户手上有一张现金券,可以兑换相应的现金,

错误示范一

 
 
 
 
  1. if(isAvailable(ticketId){ 
  2.     1、给现金增加操作 
  3.     2、deleteTicketById(ticketId) 
  4. }else{ 
  5.     return “没有可用现金券” 

解析: 假设有两条线程A,B兑换现金,执行顺序如下:

1.线程A加现金

2.线程B加现金

3.线程A删除票标志

4.线程B删除票标志

显然,这样有问题了,已经给用户加了两次现金了。

错误示范2

 
 
 
 
  1.   if(isAvailable(ticketId){ 
  2.       1、deleteTicketById(ticketId) 
  3.       2、给现金增加操作 
  4.   }else{ 
  5.      return “没有可用现金券” 
  6.   } 

并发情况下,如果一条线程,第一步deleteTicketById删除失败了,也会多添加现金。

正确处理方案

 
 
 
 
  1.   if(deleteAvailableTicketById(ticketId) == 1){ 
  2.       1、给现金增加操作 
  3.   }else{ 
  4.       return “没有可用现金券” 
  5.   } 

分享名称:日常工作中很容易犯的几个并发错误
路径分享:http://www.shufengxianlan.com/qtweb/news16/338766.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联