一、jdk并发类库
1.1 Future
Future的缺陷是没有办法异步化。
实际上他就只是获取线程的执行结果与执行状态,当需要进一步处理时,主线程需要调用get()方法。
但调用get()方法的时机很难把控,立即调用相当于串行了,很久之后调用那就白白浪费了等待的时间。
1.2 CompletableFuture
在jdk1.8,java推出了CompletableFuture。
在异步任务完成后,需要用到任务的返回结果时无需等待,直接通过thenAccept、thenApply、thenCompose等方法来对异步调用的结果进行处理。
二、业务场景
目前项目中应用到并发的场景主要是调用远程RPC接口,即IO密集型的场景。
- 第一个场景是需要分别调用两个RPC接口,无先后顺序关系,数据处理是需要同时依赖两个接口的返回数据。
- 第二个场景是需要循环调用RPC接口,调用100次以内,所有调用结束后统一对返回结果进行处理。
三、具体实现
3.1 两个接口调用后处理数据
- supplyAsync
用来执行异步操作,可以支持返回值。
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。 - thenCombineAsync thenCombine 会把 两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。
- join CompletableFuture类中的join方法和Future接口中的get有相同的含义,等待运行结束。
public List<String> findIdList(String subjectId,Integer classType) {
CompletableFuture<List<String>> aIds = CompletableFuture
.supplyAsync(() -> clientA.queryClassIdList("1", classType), TestThreadExecutor.getInstance());
CompletableFuture<List<String>> bIds = CompletableFuture
.supplyAsync(() -> clientB.queryClassIdList("2", classType), TestThreadExecutor.getInstance());
CompletableFuture<List<String>> unionClassIds = aIds
.thenCombineAsync(bIds, (aIds, bIds) -> {
aIds.addAll(bIds);
return aIds.stream().distinct().collect(Collectors.toList());
}, TestThreadExecutor.getInstance());
return unionClassIds
.exceptionally(exception -> {
System.out.println("出异常了");
return List.of();
})
.join();
}
3.2 循环调用同一个接口
- allOf allOf里面的所有线程未执行完毕,主线程会阻塞,直到allOf里面的所有线程都执行,线程就会被唤醒。
List<String> res = new ArrayList<>();
List<String> idList = Lists.of("1","2","3","4","5","6");
CompletableFuture[] cfs = idList.stream().map(object-> CompletableFuture.supplyAsync(()->client.queryClassIdList(object, classType), TestThreadExecutor.getInstance())
.thenApply(dtoList->{
if(CollectionUtils.isEmpty(dtoList)){
return "";
} else {
return claCourseId;
}
})
//如需获取任务完成先后顺序,此处代码即可
.whenComplete((v, e) ->
if(!StringUtils.isEmpty(v)){
bindClassCourseList.add(v);
}
})).toArray(CompletableFuture[]::new);
//等待总任务完成,但是封装后无返回值,必须自己whenComplete()获取
CompletableFuture.allOf(cfs).join();
3.3 自定义线程池
实际开发中我们要根据具体情况来自定义线程池,这里提供一个大概的模板。
public class TestThreadExecutor {
/**
* 线程池名称
*/
private static final String TEST_THREAD_POOL = "teacher-city-code-thread-pool";
/**
* cpu可用核数
*/
private static final int DEFAULT_CPU_PROCESSORS = Runtime.getRuntime().availableProcessors();
/**
* spring支持的线程池任务包装类
*/
private static final ThreadPoolTaskExecutor TEST_THREAD_EXECUTOR = new ThreadPoolTaskExecutor();
/**
* 默认线程池
*/
private static final ThreadFactory DEFAULT_THREAD_FACTORY = new DefaultThreadFactory(TEST_THREAD_POOL);
static {
TEST_THREAD_EXECUTOR.setThreadFactory(DEFAULT_THREAD_FACTORY);
//此线程池负责业务属于IO密集型,设置核心线程数为cpu核数*2
TEST_THREAD_EXECUTOR.setCorePoolSize(DEFAULT_CPU_PROCESSORS * 2);
TEST_THREAD_EXECUTOR.setMaxPoolSize(DEFAULT_CPU_PROCESSORS * 25);
TEST_THREAD_EXECUTOR.setQueueCapacity(1024);
//默认值就是60seconds,显示声明的目的是直观看到线程池中线程的存活时间
TEST_THREAD_EXECUTOR.setKeepAliveSeconds(60);
//CallerRunsPolicy这个拒绝策略代表的是,如果工作列队满了(超过QueueCapacity)同时MaxPoolSize也达到了阀值,那么当前任务使用主线程调用
TEST_THREAD_EXECUTOR.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//核心线程如果超过了keepTimeOut时间也会进行回收,设置这个参数的原因是因为其他maven-module可能不需要依赖这个线程池,从而造成线程资源的浪费
TEST_THREAD_EXECUTOR.setAllowCoreThreadTimeOut(true);
TEST_THREAD_EXECUTOR.initialize();
}
/**
* 获取线程池任务实例
*
* @return ThreadPoolTaskExecutor
*/
public static ThreadPoolTaskExecutor getInstance() {
return TEST_THREAD_EXECUTOR;
}
}