最新消息:文章中包含代码时,请遵守代码高亮规范!

记一次线程池使用过程中走的弯路[原创]

Java 施, 建 212浏览 0评论

Java开发应该都知道线程池是个什么东西,多线程和线程池的特性啊优点啊这里就不多说了,网上一搜一大堆,这篇博客在主要是记录一次在项目中使用线程池的经历,总结在使用过程中犯的错误.

先来讲下业务背景吧,项目背景可以理解成一个类似于商城的项目,当商城中某一个用户执行了特定操作(比如在商城里上架了一个很棒的商品).这个时候系统就需要通知商城里所有的用户,某某人上架了一个特别棒的东西 ,性价比高的一塌,赶紧过去看看.以上大概就是业务背景.

这个其实很简单,因为给某个用户发送通知的方法已经有sdk提供好实现了,只需要调用特定的api就行了,so easy.只要用户在执行刚刚说的特定操作的时候,调用一下api,发送一个通知出去,一切就完美了.但是很快问题就出现了,当商城用户量大了之后,每个用户都发送一个通知是一个非常耗时的操作,如果同步调用sdk的推送,用户体验太差了,这肯定不行,怎么办呢?

首先想到的就是使用线程池异步的去执行这个通知,springboot已经给我们封装好了线程池了,配置文件一下,轻轻松松的一个线程池就出来了,然后把发送通知的命令往线程池里一扔,让线程池慢慢执行去吧,一切又都变的如此完美,不是吗?

代码大概就是下面这个样子: (taskExecutor就是线程池了,代码中的sendMessageByToken就是发送通知的方法)

   List<BrokerPushToken> brokerPushTokens = tokenMapper.selectList(null);

        for (BrokerPushToken b : brokerPushTokens) {
            taskExecutor.submit(() -> sendMessageByToken(b));
        }

好像是完美了,不过很快问题就出现了,这个sdk提供的方法本质上实际是一次http请求发送到推送通知的服务端,服务端收到你的请求后再推送通知到用户上,那既然是http请求,那自然就会出现比如说网络原因导致的失败,我作为一个有追求有理想有抱负的没有秃顶的优秀年轻人,必然不能容忍这种事情发生了,然后就想到了使用多次失败重试的机制来避免这种偶然发生的失败请求.

然后就开始写代码了,写个for循环,只要失败了就重试,成功了就跳出循环,我真是天才啊,很快代码就写出了,大概就是下面这个样子

 
 List<BrokerPushToken> brokerPushTokens = tokenMapper.selectList(null);

        for (BrokerPushToken b : brokerPushTokens) {
            taskExecutor.submit(() -> {for (int i = 0; i < times; i++) {
            try {
                Future<Boolean> future = taskExecutor.submit(() -> sendMessageByToken(b));
                if (future.get().equals(Boolean.TRUE)) {
                    return;
                }
            } catch (Exception e) {
                log.error("run task error for {} times,exception is ", i, e);
            }
        }
});
        }

注意了,这个时候其实我已经掉坑里了,上面的代码看着好像毫无破绽,然而跟我想想的结果完全不一样,代码一执行所有的推送就全部卡死,怎么看都觉得没有问题才对,但是就是完全不工作了,当时的我真的很郁闷,我的线程池死了么,咋不干活呢,你倒是动一下啊.

几番查找加上debug工具的帮助终于找到了原因,罪魁祸首就是代码里的嵌套submit,并在第二个submit之后使用了同步获取任务结果的get方法.我来细细分析一下出现问题的步骤,当执行到for循环的时候,假如要遍历的list中有10个元素,这个是会for循环的执行相当于往线程池的任务队列中添加了10个任务,任务的内容是失败重试的方法,这个时候for循环会很快执行完成,10个任务添加到线程池之后,假如线程池中正好只有10个空闲的线程,此时正好每个线程分配到一个任务,这10个线程几乎会通知执行到Future future = taskExecutor.submit(() -> sendMessageByToken(b));这行代码,这行代码的操作就是又提交一个任务到线程池中,提交任务是一个很快的操作,几乎可以理解成往队列里添加一个元素(此时这个任务并不会马上执行而是等待有空闲的线程来执行他),后面就是坑了,这个时候这10个线程都会执行下面的future.get()这行代码,这行代码是一个等待操作,一直等到刚刚提交的future执行完成返回结果为止.这个时候问题就清晰了,你现有的10个线程都在等待任务的返回结果,但是任务是被提交到线程池的任务队列里,只有等待有空闲的线程来执行完才会返回结果,但是线程池里的10个线程都在等返回结果,没有空闲线程,因此死循环就产生了,大家都在等菜开火,但是菜需要有人去买才行.

问题找到了,改起来自然就快了,其实就是不要让线程池里的线程去等待线程池里的线程的返回结果,听起来有点绕口,简单点讲就是线程池里的多个线程尽量不要操作共享资源,可以另起一个线程池或者线程自己等待自己的执行结果.

 
   List<BrokerPushToken> brokerPushTokens = tokenMapper.selectList(null);

        for (BrokerPushToken b : brokerPushTokens) {
            taskExecutor.submit(() -> {for (int i = 0; i < times; i++) {
            try {
              Boolean re = sendMessageByToken(b);
                if (re.equals(Boolean.TRUE)) {
                    return;
                }
            } catch (Exception e) {
                log.error("run task error for {} times,exception is ", i, e);
            }
        }
});
        }

转载时请注明出处及相应链接,本文永久地址:https://blog.yayuanzi.com/24851.html


pay_weixin
pay_weixin
微信打赏
pay_weixin
支付宝打赏
感谢您对作者joy1的打赏,我们会更加努力!    如果您想成为作者,请点我

发表我的评论
取消评论

表情