2025年3月20日 星期四 甲辰(龙)年 月十九 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Java

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

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

项目场景:

  • 上游接口批量推送订单信息,订单外面还有运单信息(一个运单包含多个订单),订单和运单都允许有条件的修改。当接口收到推送过来的数据时,要先去查询这个订单对应的运单是否已经存在,不存在则直接把运单信息插入到数据库表中,存在要做判断运单的状态是否允许更新,不允许直接抛异常。
  • "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)。
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门