java代码中一眼不容易发现的bug

最近新需求相对少一些,于是我又成了职业擦屁股人,于是吧,就成了千篇一律的排查sql,处理循环中的查询,大事务,这些常见问题,偶尔遇到一两个一眼看不出问题所在的,反而觉得有意思多了,比如下面这两个问题:

1:先上一个简单的,长时间卡顿问题,伪代码如下
//数据库查询数据
List a = service.findDataByCondition1();
//换个条件继续查询数据
List b = service.findDataByCondition2();
//剔除a中在b里存在的数据
a.removeAll(b);

简化成这个样子应该成猜到哪里出问题了,

当时排查,首先就是习惯性去排查了sql,发现sql挺简单,命中索引,虽然数据有点多,但是也不至于卡顿这么久,

(看上去很简单的sql,也被坑过:当时是mySql 5.x 版本,看上去是单表查询,实际上这个“单表”是个很复杂的视图,视图本身能用到索引, 但是加了where条件后,相当于在视图查询的结果集外再包一层然后加查询条件,直接卡爆)

然后打了日志,大概定位到这一段代码,定眼一看,幡然醒悟:

List 的 removeAll  方法,这个东西性能应该不太好吧,相当于两个循环,挨个调用equals(), 对象越复杂,equals 比较时间越长,数据越多嵌套循环性能越差。 进一步优化,这个查询可以根据一些条件尽量分批次去查询,节约内存

具体修改方法也特别简单,直接展示测试结果,

创建一个简单测试类

public static class Node {
        int a;
        String b;
        int c;
        String d;
        public static Node createNode(int numer) {
            return new Node(numer, "测试" + numer, numer + 100, "其他测试" + numer);
        }
    }

分别在 数组和HashSet中添加100000个 Node 对象,然后进行移除

2: 线上卡死问题

这次是在一个界面输入某个条件查询列表后,一直不出来数据,界面一直转“菊花”,还是老规矩,看了sql,一切正常。但是有一段看上去不太和谐的代码感觉有点问题,大概如下

CompletableFuture<String> completableFuture = new CompletableFuture<>();

//根据一些条件查询数据库
List list = service.find(query);
if (CollectionUtils.isNotEmpty(list)) {
       completableFuture = CompletableFuture.supplyAsync(() -> {
            //做一些耗时操作
            return doSomeThing();
        });
}

//这里只是举例只写了一个,allOf中还有其他的 completableFuture
CompletableFuture<Void> allOf = CompletableFuture.allOf(completableFuture);

allOf.get()

首先看上去写的还挺仔细,第一行代码先赋初始值,防止空指针,

把几个耗时操作异步进行,最后用 allOf.get(); 等待所有任务完成 (我习惯用whenComplete() 这个方法)

问题就出在 if 中的判断条件,如果数组为空,也就是界面的某个条件查不到数据时。allOf.get() 这行代码会一直阻塞!

查看源码,代码调用为  

CompletableFuture#waitingGet   ->  ForkJoinPool.managedBlock(q);

然后循环中判断  blocker.block()

blocker.block() 中会进行阻塞,等待任务完成唤醒线程

源码里唤醒线程则是:任务完成会调用这个方法:

final void postComplete()

然后调用 tryFire 方法,这里就能唤醒阻塞线程

所以问题就出在,直接new 一个CompletableFuture,当list为空就回导致没有提交一个任务,就没有了任务完成唤醒线程的操作。导致页面表现为卡死

3:类似问题

这几天吧,外部系统调用我们的接口,老是超时,那么,是不是又是数据库出问题了呢?老规矩,挨个sql排查,老演员数据库欲哭无泪啊,老是我数据库大哥背锅,

然后呢,万千代码中相中了一个老熟人, CompletionService,这个工具类我自己还没用过,,,,但是长得和 CompletableFuture 有几分神似。就从它入手吧,然后大概还原的伪代码如下:

public static void main(String[] args) throws Exception {
        List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6);
        CompletionService<Object> completionService = ThreadUtil.newCompletionService();
        for (Integer number : list) {
            if (number > 2) {
                completionService.submit(() -> {
                    Thread.sleep(1000);
                    log.info("完成了!!");
                    return number + 100;
                });
            } else {
                log.info("不符合条件,打个日志记录下");
            }
        }

       list.forEach(number ->{
           try {
               completionService.take().get(10000, TimeUnit.SECONDS);
           } catch (Exception e) {
               e.printStackTrace();
           }
       });

        System.out.println("结束了");

    }

这段代码看起来还挺靠谱,completionService.take().get(10000, TimeUnit.SECONDS)   方法中还加了等待时间,防止无线等待,但是跑起来,永远等不到System.out.println("结束了"); 这行代码执行。因为问题不是出在 .get(10000, TimeUnit.SECONDS) 方法,而是take()

原因其实和问题2类似, 6个元素的list,只放入了4个任务,然后又遍历整个list 6次,来获取任务的结果 ,那还有两次就永远等不到响应。这个工具类理解成 生产消费者问题就行。

CompletionService内部有一个阻塞队列,消费者是completionService.take(),就是去阻塞队列中获取 Future, 而生产者则是 completionService.submit,向队列放入Future,当生产者发放入的数量少于take()获取的数据量时,take()就会等待,直到有新的任务进入。

比如把上面的代码改一下,加一个异步线程丢入两个任务,completionService.take().get() 就能正常退出

 public static void main(String[] args) throws Exception {
        List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6);
        CompletionService<Object> completionService = ThreadUtil.newCompletionService();
        for (Integer number : list) {
            if (number > 2) {
                completionService.submit(() -> {
                    Thread.sleep(1000);
                    log.info("完成了!!");
                    return number + 100;
                });
            } else {
                log.info("不符合条件,打个日志记录下");
            }
        }

        //等待一段时间后再放入两个任务
        new Thread(() -> {
            try {
                Thread.sleep(9000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            for (int i = 0; i < 2; i++) {
                completionService.submit(() -> {
                    Thread.sleep(1000);
                    log.info("其他线程任务完成了!!");
                    return 0;
                });
            }

        }).start();

       list.forEach(number ->{
           try {
               completionService.take().get(10000, TimeUnit.SECONDS);
           } catch (Exception e) {
               e.printStackTrace();
           }
       });

        System.out.println("结束了");

    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值