一、背景
前期开发了一个列表查询接口,返回内容中有一些需要去查询C服务来补全内容。为了提高列表查询接口的性能,需要对列表中的内容进行并发查询C服务来缩短全量列表任务的执行时间。
因为JDK8中有并行流处理方法,此处是对列表中的内容进行查询补齐,使用并行流处理方式正好能够完成这个任务,于是就使用了parallelStream来处理。伪代码如下:
public List<Detail> list(){
//从DB中查询到数据
List<Detail> detailList = Stream.of(Detail.builder().id("1").name("name1").build(),
Detail.builder().id("2").name("name2").build(),
Detail.builder().id("3").name("name3").build(),
Detail.builder().id("4").name("name4").build(),
Detail.builder().id("5").name("name5").build(),
Detail.builder().id("6").name("name6").build(),
Detail.builder().id("7").name("name7").build()).collect(Collectors.toList());
log.info("Detail get, size is: {}", detailList.size());
detailList.parallelStream().forEach(
detail -> {
log.info("userId:{}", BlogRequestContext.getCurrentContext().getUserId());
try {
// query C server to get detail
Thread.sleep(3000);
}catch (Exception e){}
detail.setUserId(BlogRequestContext.getCurrentContext().getUserId());
}
);
return detailList;
}
parallelStream提供了比较方便的列表并行处理方式,当时在测试环境验证通过后就上线了。运行一段时间后出现了一个用户查询列表后,数据中C服务内容缺失了,但实际改用户的资源在C服务中是有记录的。
二、问题分析
代码中查询C服务时,会传入用户信息,通过用户信息及其他条件来查询当前用户在C服务的数据。
通过排查日志,发现在请求C服务时,传入的用户信息是另一位用户的,用户信息串用了。
而代码中的用户信息是通过线程上下文传递的,在主线程中用户信息没有串,但是通过parallelStream处理的线程就出现了线程上下文串用的情况,究其原因,还是线程池在处理完任务后,线程没有被立即回收,同时又被调度去处理其他的任务了,此时线程里的上下文信息还是上一个任务的,导致上下文串用了。
至此基本定位到问题了。
那解决方案也就出来了,也就是在用parallelStream时,从主线程将上下文信息拿出来,在parallelStream的子线程中处理时,将这些主线程的上下文再重新reset一下,保证子线程的上下文和主线程的一致。
三、parallelStream的线程池是怎么样的
默认情况下,parallelStream使用的是ForkJoinPool.commonPool(),这是一个共用的线程池,也就是工程的JVM都共用这个公共线程池,除了parallelStream外,还有completablefuture也使用了这个公共线程池。
这个公共线程池,线程池数量是当前CPU核数-1,减1是因为当前主线程会占用一个线程,线程池中的CPU核数-1如果是大于1的,就会新建这么个公共线程池,如果小于等于1的话,系统就会新建一个线程来处理,而不是线程池。
如果是新建一个线程来处理,并发量特别大的情况,工程会不断的新建线程,可能会导致线程资源耗尽。
总结:parallelStream和completablefuture都会使用ForkJoinPool.commonPool()的公共线程池。
四、线程池中如何避免线程串用
工程中自定义的线程池也会存在线程串用的情况,一般我们的解决办法是使用阿里巴巴的transmittable-thread-local依赖,在定义线程池时进行设置。
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("bService_");
executor.setCorePoolSize(processors);
executor.setMaxPoolSize(processors << 1);
executor.setKeepAliveSeconds(30);
executor.setAllowCoreThreadTimeOut(true);
executor.initialize();
return TtlExecutors.getTtlExecutor(executor);