您当前的位置:首页 > 计算机 > 编程开发 > Java

项目实战:一个由多线程引起的线程安全问题(附:解决方案)

时间:05-18来源:作者:点击数:

项目场景:

  • 上游接口批量推送订单信息,订单外面还有运单信息(一个运单包含多个订单),订单和运单都允许有条件的修改。当接口收到推送过来的数据时,要先去查询这个订单对应的运单是否已经存在,不存在则直接把运单信息插入到数据库表中,存在要做判断运单的状态是否允许更新,不允许直接抛异常。
"submitList": [
    {
      "deliveryCode": "YD12345678"
      "xdockOriginOrderDTOS": [
        {
          "deliveryCode": "YD12345678",
          "orderCode": "DD12345677"
        }
      ]
    },
    {
      "deliveryCode": "YD12345678"
      "xdockOriginOrderDTOS": [
        {
          "deliveryCode": "YD12345678",
          "orderCode": "DD12345688"
        }
      ]
    }
  ]

问题描述

  • 当上游一次推送属于一个运单下的两个订单过来的时候,如果没有在数据库表的运单号字段添加唯一索引,会出现两条运单号一样的数据。如果运单号字段添加了唯一索引,则会抛["\n### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '462479945432663' for key 'goods_xdock_package.goods_xdock_package_delivery_code_uindex'"]这个异常。
  • 出现线程安全问题的批量新增运单的代码:
// submitList 结构见上面,只展示和问题相关的字段
public Boolean batchAddOriginOrder(List<XdockPackageDTO> submitList) {
	for (XdockPackageDTO submitDTO : submitList) {
	   try {
	       this.addPackage(submitDTO);
	   } catch (Exception e) {
	       log.error(e.getMessage());
	       e.printStackTrace();
	   }
	}	
} 
public Boolean addPackage(XdockPackageDTO xdockPackageDTO) {
	if (Strings.isNotBlank(xdockPackageDTO.getDeliveryCode())) {
        XdockPackage one = xdockPackageService.getOne(new LambdaQueryWrapper<XdockPackage>().eq(XdockPackage::getDeliveryCode, xdockPackageDTO.getDeliveryCode()));
        if (Objects.nonNull(one)) {
            Assert.isTrue(PackageStatusEnum.CREATED.getCode().equals(one.getStatus()), "运单已签收,不允许修改");
        } else {
			XdockPackage xdockPackage = XdockConvert.INSTANCE.xdockPackage2DTO(xdockPackageDTO);
	        xdockPackageService.save(xdockPackage);
		}
    }
}

原因分析:

  • 上面addPackage()方法主要包含3个步骤:
    • a. 查询数据库表中是否存在当前传入的运单号
    • b. 判断查询结果是否为空
    • c. 不为空就判断运单状态,为空就新增运单信息
      在这里插入图片描述
  • 循环遍历列表for (XdockPackageDTO submitDTO : submitList),创建两个线程(线程1线程2)分别执行两条订单信息的创建。
  • 线程1先去执行步骤a(a. 查询数据库表中是否存在当前传入的运单号),然后执行步骤b(b. 判断查询结果是否为空)。由于CPU的切换关系,此时CPU的执行权被切换到了线程2。线程1将步骤b的判断结果等信息保存到线程1的工作内存中,就处于就绪状态,线程2处于运行状态。
  • 线程2也需要执行步骤a,由于线程1没有对运单号做新增操作。因此此时线程2执行步骤b查询到的结果也为空。
  • 此时CPU的执行权切换到了线程1上。线程1将工作内存中的之前存储的查询结果等数据恢复,执行步骤c(c. 为空就新增运单信息),插入运单号。线程1执行完毕,线程1销毁,
  • CPU执行线程2,线程2将工作内存中的之前存储的查询结果等数据恢复,执行步骤c(c. 为空就新增运单信息),插入运单号。如果没有在数据库表的运单号字段添加唯一索引,会出现两条运单号一样的数据。如果运单号字段添加了唯一索引,则会抛["\n### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '462479945432663' for key 'goods_xdock_package.goods_xdock_package_delivery_code_uindex'"]这个异常。

解决方案:

Lock 锁

private final ReentrantLock lock = new ReentrantLock();
public Boolean addPackage(XdockPackageDTO xdockPackageDTO) {
	  if (Strings.isNotBlank(xdockPackageDTO.getDeliveryCode())) {
	      lock.lock();
	      try {
	          XdockPackage one = xdockPackageService.getOne(new LambdaQueryWrapper<XdockPackage>().eq(XdockPackage::getDeliveryCode, xdockPackageDTO.getDeliveryCode()));
	          if (Objects.nonNull(one)) {
	              Assert.isTrue(PackageStatusEnum.CREATED.getCode().equals(one.getStatus()), "运单已签收,不允许修改");
	          } else {
				  XdockPackage xdockPackage = XdockConvert.INSTANCE.xdockPackage2DTO(xdockPackageDTO);
			      xdockPackageService.save(xdockPackage);
			}
	      }catch (Exception e) {
	          e.printStackTrace();
	      }finally {
	          lock.unlock();
	      }
	  }
}

synchronized锁

synchronized (this) {
   XdockPackage one = xdockPackageService.getOne(new LambdaQueryWrapper<XdockPackage>().eq(XdockPackage::getDeliveryCode, xdockPackageDTO.getDeliveryCode()));
    if (Objects.nonNull(one)) {
        Assert.isTrue(PackageStatusEnum.CREATED.getCode().equals(one.getStatus()), "运单已签收,不允许修改");
    } else {
        XdockPackage xdockPackage = XdockConvert.INSTANCE.xdockPackage2DTO(xdockPackageDTO);
        xdockPackageService.save(xdockPackage);
    }
}

synchronized 与 Lock 的区别

synchronized 和 Lock 的用法区别

  • synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized 可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
  • lock(显示锁):需要显示指定起始位置和终止位置。一般使用 ReentrantLock 类做为锁,多个线程中必须要使用一个 ReentrantLock 类做为对象才能保证锁的生效,且在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。

synchronized 和 Lock 性能区别

  • synchronized 是托管给 JVM 执行的,而 Lock 是 Java 写的控制锁的代码。在 JDK 1.5 中,synchronize 是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用 Java 提供的 Lock 对象,性能更高一些。但是到了 JDK 1.6,synchronize 进行很多优化,比如锁升级无锁 → 偏向锁(JDK15已废弃) → 轻量级锁 → 重量级锁)等等。所以在 JDK 1.6 以上 synchronize 的性能并不比 Lock 差。

synchronized 和 lock 机制区别

  • synchronized 原始采用的是 CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。
  • Lock 用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门