记一次线程池的故障,使用线程池该如何处理异常?

1 业务背景

点赞业务:之前公司各个业务线的的点赞记录只存一条记录到数据库,并没有汇总每个资源的点赞总数,这样的话等到缓存失效时候,就需要去数据库中进行count()查询。随着数据量越来越大,count()的成本肯定会越来越高。所以想通过程序计算总数写入数据库的一张新表。等到缓存失效之后,用这张表的数据进行兜底做被动缓存。
由于目前的数据量在9800多万,如果一条一条的进行同步计算,耗费的时间比较长,所以就用异步的方式,同时去处理不同业务线的数据。
采用for循环的方式一次从数据库中查出500条数据进行处理,但是发现每次只能执行一轮,然后就停止了。仔细看了一下代码,发现了问题(数据库唯一键冲突 😂)。不过异常信息并没有正常输出,于是就研究了一下线程的异常打印。

2 业务实现

线程池的创建

package net.csdn.interaction.favorite.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 线程池管理
 * Created by haoll on 2020/8/17
 */
@Slf4j
@Configuration
public class ThreadPoolConfiguration {


    @Bean
    public ThreadPoolExecutor dataScriptThreadPool() {
        SynchronousQueue<Runnable> workQueue = new SynchronousQueue<>();
        ThreadFactory threadFactory = new ThreadFactory() {
            private AtomicInteger threadNumber = new AtomicInteger(0);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "dataScript" + threadNumber.incrementAndGet());
            }
        };
        return new ThreadPoolExecutor(
                1, 5, 180,
                TimeUnit.SECONDS,
                workQueue,
                threadFactory,
                (r, executor) -> log.error("dataScriptException"));
    }
}

同步方法

/**
     * 同步接口
     * @param startId
     * @param endId
     * @param appId
     * @return
     */
    @GetMapping("syncLikeTotalForIncrement")
    public Result<Void> syncLikeTotalForIncrement(@RequestParam("startId")Long startId, @RequestParam("endId")Long endId, @RequestParam("appId")Long appId){

        int total = userLikeDAO.countById(startId, endId,appId);
        log.info("total={}",total);
        dataScriptThreadPool.submit(() -> {
            dealSyncLikeTotal(total, startId, endId,appId);
            log.info("syncLikeTotal.end");
        });
        return Result.buildResult();
    };

3 线程池的异常处理

疑问又来了,为什么使用线程池的时候,线程因异常被中断却没有抛出任何信息呢?还有平时如果是在 main 函数里面的异常也会被抛出来,而不是像线程池这样被吞掉。
如果子线程抛出了异常,线程池会如何进行处理呢?

我提交任务到线程池的方式是: dataScriptThreadPool.submit(Runnbale task); ,后面了解到使用 execute() 方式提交任务会把异常日志给打出来,这里研究一下为什么使用 submit 提交任务,在任务中的异常会被“吞掉”。

对于 submit() 形式提交的任务,我们直接看源码:

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    // 被包装成 RunnableFuture 对象,然后准备添加到工作队列
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

它会被线程池包装成 RunnableFuture 对象,而最终它其实是一个 FutureTask 对象,在被添加到线程池的工作队列,然后调用 start() 方法后, FutureTask 对象的 run() 方法开始运行,即本任务开始执行。

public void run() {
    if (state != NEW || !UNSAFE.compareAndSwapObject(this,runnerOffset,null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                // 捕获子任务中的异常
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        runner = null;
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

在 FutureTask 对象的 run() 方法中,该任务抛出的异常被捕获,然后在setException(ex); 方法中,抛出的异常会被放到 outcome 对象中,这个对象就是 submit() 方法会返回的 FutureTask 对象执行 get() 方法得到的结果。
但是在线程池中,并没有获取执行子线程的结果,所以异常也就没有被抛出来,即被“吞掉”了。这就是线程池的 submit() 方法提交任务没有异常抛出的原因。

4 线程池自定义异常处理方法

  1. 因为ThreadPoolTaskExecutor.submit 方法, 这个方法的返回值是 Future 类型. 默认情况下, 它只有在调用 get() 方法时才会返回处理结果以及异常检查。所以将 submit() 修改为 execute()既可以打印出来异常信息。

  2. 直接在代码中进行try catch()操作,这样也可以捕获打印异常。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值