1. 创建线程池
- 打开spring异步开关,在启动类上加
@EnableAsync
注解; - 创建线程池组件:
@Configuration
public class ExecutorConfig {
// 优先取配置文件中executor.thread.core_pool_size属性的值,不存在则默认20
@Value("${executor.thread.core_pool_size:20}")
private int corePoolSize;
@Value("${executor.thread.max_pool_size:500}")
private int maxPoolSize;
@Value("${executor.thread.queue_capacity:100}")
private int queueCapacity;
@Value("${executor.thread.name.prefix:executor}")
private String namePrefix;
@Value("${executor.thread.keep.alive.seconds:60}")
private int keepAliveSeconds;
@Bean(name = "asyncServiceExecutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 线程池各个参数含义自行百度
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setThreadNamePrefix(namePrefix);
executor.setKeepAliveSeconds(keepAliveSeconds);
// 配置传递上下文信息的装饰器,后面详细解释
executor.setTaskDecorator(new WebContextTaskDecorator());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 执行初始化
executor.initialize();
return executor;
}
}
2. 控制层
@RestController
public class DoController {
@Autowired
private DoService service;
@RequestMapping(value = "/doBatch", method = RequestMethod.POST)
public JsonResponse<ResVO> doBatch(@RequestBody ReqVO reqVO) {
ResVO resVO = service.doBatch(reqVO);
return JsonResponse.success(resVO);
}
}
3. 服务层
控制层直接调用的服务类:
@Service
public class DoServiceImpl implements DoService {
@Autowired
private DoServiceByMultiThread doServiceByMultiThread ;
@Override
public ResVO doBatch(ReqVO reqVO) {
// length为需要执行异步任务的总数,比如10笔下单等
int length = 10;
// 同步锁,用于阻塞当前请求线程直到异步线程执行完毕
CountDownLatch lock = new CountDownLatch(length);
// 将结果类传进去用于保存业务正常执行后的结果与线程异常信息
ResVO resVO = new ResVO();
// 使用线程安全的容器
resVO.setSuccessList(Collections.synchronizedList(new ArrayList<>()));
resVO.setErrorList(Collections.synchronizedList(new ArrayList<>()));
for (int i = 0; i < length; i++) {
doServiceByMultiThread.service(resVO, lock);
}
try {
// 阻塞请求线程直到异步线程全部完成
lock.await();
} catch (InterruptedException e) {
LogUtil.info("多线程异常");
}
}
}
结果类ResVO:
public class ResVO {
// 操作成功列表
List<SingleResult> successList;
// 操作失败列表
List<SingleResult> errorList;
public static class SingleResult {
// 操作序列号等其他业务需要的信息
private String serialNo;
// 错误原因,仅线程抛异常时存在
private String reason;
}
}
执行异步操作的服务:
@Component
public class DoServiceByMultiThread {
// @Async注解的方法将被spring代理后异步执行,可通过参数指定使用的线程池
// 该方法不能与调用方位于同一个类中,且返回值只能为void或FutureTask
@Async("asyncServiceExecutor")
public void service(ResVO resVO, CountDownLatch lock){
SingleResult result = new SingleResult();
try {
// 获取上下文信息,如果上下文使用的是ThreadLocal,可能会有空指针异常
// 因为这里的线程并非请求线程,父线程的上下文会丢失,通过后面的TaskDecorator解决该问题
WebContext webContext = WebContext.getWebContext();
// 可能会抛出异常的业务操作......
LogUtil.info("编号xxx的异步任务执行成功");
result.getSuccessList.add(result);
}
catch (Throwable e) {
// 捕获子线程异常并打印,必须使用日志工具类中带异常对象的error方法,否则无法打印完整堆栈信息
LogUtil.error(e, "编号xxx的异步任务执行失败,错误原因:");
result.setReason(e.getMessage() == null ? "系统内部错误" : e.getMessage());
result.getErrorList.add(result);
} finally {
// 异步任务完成数+1,必须放在finally块中,否则可能导致请求线程永远阻塞
lock.countDown();
}
}
}
4. 上下文类
// 保存了本次请求与响应的上下文,一般在拦截器中初始化
public class WebContext {
// 通过ThreadLocal与线程绑定
private static ThreadLocal<WebContext> localContext = new ThreadLocal();
private HttpServletRequest request;
private HttpServletResponse response;
public WebContext(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
}
public HttpServletRequest getRequest() {
return this.request;
}
public HttpServletResponse getResponse() {
return this.response;
}
public static void initWebContext(HttpServletRequest request, HttpServletResponse response) {
WebContext context = new WebContext(request, response);
initWebContext(context);
}
public static void initWebContext(WebContext webContext) {
localContext.set(webContext);
}
public static WebContext getWebContext() {
return (WebContext)localContext.get();
}
public static void clear() {
localContext.remove();
}
}
5. 用于父子线程传递上下文信息的装饰类
// 在初始化线程池前作为参数设置进去,从而让线程池的线程执行任务时可以访问WebContext
public class WebContextTaskDecorator implements TaskDecorator {
/**
* @param runnable 执行异步任务的线程池中的线程
* @return 装饰后的线程(即绑定WebContext后的)
*/
@Override
public Runnable decorate(Runnable runnable) {
// 此处获取的是与请求线程绑定的WebContext,绑定一般在拦截器中实现
WebContext webContext = WebContext.getWebContext();
return () -> {
try {
WebContext.initWebContext(webContext);
runnable.run();
} finally {
// 清除当前线程的WebContext
WebContext.clear();
}
};
}
}
6. 总结
- 通过CountDownLatch实现阻塞请求线程直到异步任务执行结束;
- 向异步任务传列表(必须是线程安全的容器)入参,统计任务成功与失败条数,并保存失败原因;
- 使用TaskDecorator在父子线程中传输上下文信息,让异步任务执行时可以访问类似WebContext的上下文