java深度调试技术【第四五章:多线程和幽灵代码】

2025-12-12 0 163

前言

多线程幽灵代码

这次介绍的是第四章第五章的内容,因为这两章的内容比较少我就把他合到一起了
,这两章有些内容比较基础吧.
比如第四章写了 如何加锁,如果正确的使用synchronized,以及如何实现线程的通信这些算是比较基础的知识吧。
第五章的主题是幽灵代码,介绍得也非常得基础,介绍了异常情况导致的资源泄露,比如释放资源没有在finally中执行,双检索问题。(所以这两章也是非常的基础)

幽灵代码(防御性编程)

既然书上的案例太基础了,那就自己总结一波吧

1. 数据库的时间精度问题

datetimetimestampmysql 中 默认精度是秒(不设置精度的话就会造成精度丢失),我们使用java 的时间对象保存的时候如果把毫秒带上这样对于敏感的业务,就会受到影响,因为后面的毫秒在插入数据库的时候会四舍五入 ,比如 当前时间 23点59m59s600ms 存到数据库就会变成 第二天0晨。

2.异常的处理

2.1 RabbitMQ 消费不捕捉异常

消费代码中未捕捉异常,并且不配置死信息队列和重试次数,因为在开发测试过程中异常的情况很少碰到,上了生产环境运行一段时间可能就会出现异常的情况吗,比如消费逻辑中有RPC 或者 Http 接口 出现异常等问题。这样我们的消息就会一直在队列中一直消费。

@RabbitListener(queues = RabbitMqConstant.SMS_DELAY_QUEUE) 
public void id(Message message, Channel channel ) {
    String taskId = new String(message.getBody()); 
    log.info(\"-------task ID-------:{}\",taskId); 
    STask sTask = taskService.getBaseMapper().selectById(id); 
    if(Objects.nonNull(sTask)){ 
        log.info(\"-------schedule action sTask-------:{}\", sTask);
        taskService.commitTaskAfterDoSend(sTask);
    }
}

2.2 线程池任务不捕捉异常(CountDownLatch )

线程池任务不捕捉异常,并且使用了CountDownLatch 等到任务执行完成,如果执行过程中发生异常就会出现 主线程一直被阻塞

// 伪代码--------
@Scheduled  
void countMsg(){
    if(RedisUtils.lock){
      CountDownLatch countDownLatch = new CountDownLatch(size);
        for(){
            threadPoolExecutor.execute(()->{
                //没有捕捉异常
                countMethod();
                countDownLatch.countDown();
              });
        }
      //一直阻塞
      countDownLatch.await();
      log.info(\"统计时间:\");
      RedisUtils.releaseLock();   
    }
}

3 事务问题

事务失效的基本场景就不说了,相信大家都背得很熟了,主要说一下事务中异步问题和锁问题。

3.1 事务里面嵌套分布式锁

这个问题是很多老手都容易犯的问题,很多公司对并发测试这块要求并不严格,所以这个问题很容易蒙混过关发到生产环境

@Transactional(rollbackFor = Exception.class)
public void importData2(Long importId) {
  RLock lock = redissonLockClient.getLock(RedisKeyConstant.MEMBER_IMPORT_LOCK_KEY);
  try {
    lock.lock();
    // 读取导入记录ID,获取导入文件地址,解析数据,数据校验,数据分组
    GroupData data = handleData(importId);
    //插入
    batchInsert(data.getNeedInsertData());
    //更新导入记录成功
    updateSuccessImportRecord(importId);
  } catch (Exception e){
    //表导入记录失败
    updateFailImportRecord(importId);
  }finally {
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }
}
3.2事务中的异步操作

这个问题是也是经常出现,可能存在异步代码执行了,最后事务执行失败。或者是异步操作依赖事务的结果这样可能异步操作执行了时候 事务还没提交完成,导致异步执行逻辑出问题。

@Transactional(rollbackFor = Exception.class)
@Override
public Respoonse updateAccountRoles(BatchUpdateParam param) {
    //校验参数,
    if(!paramCheck(param)){
        return Response.fail(\"网络繁忙!\");
    }
    // 更新账号信息。
    List updateUserList = userService.updateRoles(param);
    if (updateUserList.size() > 0) {
        // 异步通知其他系统更新对应账号信息。
        //事务还没提交,以及已经开始异步执行了,异步方法里面可能查询还是更新前的数据。
        userSyncUtil.asyncUserList(updateUserList);
    }
    return Respoonse.ok(\"批量更新成功。\");
}

4.数据库操作

关于SQL的一些不规范操作或者查询今天就不说,介绍两个我在工作中遇到的两个问题吧,一个 mybatis-plus的查询和pageHelper的使用问题。

4.1 Mybatis-plus 数据查询

Wrapper 查询数据没有指定需要的字段,所有等同于 select * 这也是有些公司不用mybatis-plus的原因之一,当然Wrapper 是可以指定查询的字段的,只是开发中很多人为了方便或者都是分页查询,查询出来的数据量不大所以就没有啥问题。

错误案例(能确认list数据一定不多的话,问题倒是不大):

public List selectByShopIdList(List shopIdList) {

    Wrapper wrapper = new EntityWrapper();
    wrapper.in(\"shop_id\", shopIdList);
    List shopAddressList = shopAddressMapper.selectList(wrapper); 
    return shopAddressList;

}

指定返回需要的字段

shopAddressMapper.selectList(
        Wrappers.lambdaQuery()
           //返回指定字段
            .select(ShopAddress::getShopId, ShopAddress::getAddress, ShopAddress::getCity)
            .in(ShopAddress::getShopId, shopIdList)
    );
}
4.2 pageHelper 的使用

之前一个高级java开发就犯过这个问题,开启分页之后,判断条件之后直接返回空集合了,没有执行查询语句,导致这个分页信息依然和 当前线程绑定。当线程被复用时第一个SQL 就会被当成分页查询出来,出现报错等情况。

就是开启分析之后,将分页信息和当前线程绑定了,所以我们再开启分页之后,一定要保证分页SQL的执行(执行之后线程绑定的分页信息)或者把 清除线程的分页信息(调用clear方法)。

错误案例

PageHelper.startPage(1,10);
//当条件不满足时,不执行
if(condition){
     List banners = saBannerMapper.queryList();
}
return Collections.emptyList();

5.线程上下文变量在线程池中传递

我们系统用户授权还是走的shiro那一套,shiro 会把用用户信息 通过 InheritableThreadLocal 进行绑定到上下文中,当我们在线程池中也 通过 shiro 的上下文去获取的话,这时候线程池的用户信息就是创建线程时的父线程的用户信息了。这个问题会造成获取到的用户信息错乱的问题,在实际开发过程我也遇到过好几次。

错误使用示例: 假设我们 userContext 就是 shiro 获取用户信息的上下文

static Executor executor = Executors.newSingleThreadExecutor();

public static void main(String[] args) {
 
    //1. 发起请求1:假设现在是 用户 张三 发起请求 
    userContext.set(new UserContext(\"u123\", \"张三\"));
    executor.execute(()->{
        System.out.println(\"子线程获取到的上下文: \" + userContext.get());
    });
    // 模拟请求1执行完成
    Thread.sleep(1000);
    // 2. 发起请求2:假设现在是 用户 李四 发起请求
    userContext.set(new UserContext(\"123\", \"李四\"));
    System.out.println(\"父线程上下文: \" + userContext.get());
    executor.execute(()->{
        System.out.println(\"子线程获取到的上下文: \" + userContext.get());
    });
    // 清理资源
    userContext.remove();

}

输出如下:

子线程获取到的上下文: UserContext{userId=\'u123\', userName=\'张三\'}
父线程上下文: UserContext{userId=\'123\', userName=\'李四\'}
子线程获取到的上下文: UserContext{userId=\'u123\', userName=\'张三\'}

我们第二次 从线程池获取用户信息,发现拿到的是第一次绑定的用户信息,这就造成用户信息错乱了

6. 缓存问题

缓存这个问题,案例就太多了,相信大家背过很多八股文了,缓存一致性问题等。今天给大家分享一个在实际开发中遇到的mybatis 一级缓存的案例:

错误示例:

List ret = mapper.select(Obejct parameter); 
ret.add(1234l)
.....
//中间很多其他操作和方法,再次查询 结果就被修改了
List ret2 = mapper.select(Obejct parameter); 

7. 并发问题(分布式锁失效->数据库兜底)

这个也是非常常见的问题,今天就分享几个并发兜底的措施吧。CRUD开发中也能用上的,很多时候我们以为加了分布式锁就高枕无忧了,我遇到过几次都是因为加了分布式锁也出现了数据问题。

分布式锁过期、事务提交之后数据响应超时->异常->分布式解锁->事务实际上还在执行(此时同样的请求再进来就出错)

7.1 更新操作

数据的状态更新 以及库存的扣减,这些算是CRUD 开发中经常遇到的更新操作,但是我看了很多人写的代码都存在些小问题吧。

不完美的示例:

// 事务外层加了分布式锁

@Transactional(rollbackFor = Exception.class) 
@Override public void commit(String id) { 
    User user = UserUtil.getUser(); 
    if(this.getId(id).getStatus !=1){
        thorw new RuntimeException(\"状态校验失败?不能提交数据\");
    }
    this.update(Wrappers.lambdaUpdate(OfflineActivityEntity.class)
    .eq(OfflineActivityEntity::getId,req.getId()) 
    //更新状态 
    .set(OfflineActivityEntity::getStatus, 2);
    OfflineActivityEntity OfflineActivityEntity = activityMapper.selectById(id);
    //提交审批数据(会生成一条审批数据) 
    service.commitApprove(OfflineActivityEntity, user);
}

7.2 插入操作

数据重复插入,很多时候也只在代码层面上加了锁和查询记录是否存在的操作。但是数据库没有做相关的唯一索引(比如本身业务上规定 一个人只能存在一条数据)。

没有加唯一索引大概有两个原因,第一是信任分布式锁,第二是因为数据库的设计 不太好加唯一索引,比如数据库存在is_del 的字段,这时候加了唯一索引 数据逻辑删除之后 也不能插入正常的数据了。

如果数据正确性第一,那么我们就必须把唯一索引加上,要么用单独的表记录逻辑删除的数据;

要么逻辑删除标识上加上版本号措施,比如0正常,其他标识删除,每次删除把逻辑删除的值改成ID值,这样就可以做联合唯一索引

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 编程相关 java深度调试技术【第四五章:多线程和幽灵代码】 https://www.zuozi.net/35867.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务