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 线程池自定义异常处理方法
-
因为ThreadPoolTaskExecutor.submit
方法, 这个方法的返回值是Future
类型. 默认情况下, 它只有在调用get()
方法时才会返回处理结果以及异常检查。所以将submit()
修改为execute()既可以打印出来异常信息。
-
直接在代码中进行try catch()操作,这样也可以捕获打印异常。