程序猿大吉在实施的威逼之下又有了新的需求,翻译成技术语言,大致是这样的:
不停地调用一个远程接口(成千上万次)。该接口会返回一串id,并拿着这个id回写我们本地的数据库。
这个远程接口响应时间特别久,大概要1到3s。而一旦接口返回一串id,并将id回写到本地数据库,这个过程比较短,只需要0.05s左右。
所以我想到了将查询远程接口封装成一个函数,将回写本地数据库封装成一个函数,将异常处理封装成一个函数。这样可以最大程度解耦。
在保证效率的情况下必须是用线程池,使用线程池的话,有两种方法解决该需求:
这两种方法在执行顺序上其实是完全一致的,效率也差不多。如果考虑代码可读性,则可以选用方法一。但是方法二在逻辑上更说得通,且在多人协作开发的情况下,线程功能更加单一,耦合度要低一些。
最终选用了方法二作为开发方案。下面就来重点讨论 CompletableFuture 的技术实现。
使用 CompletableFuture 开启一个线程,该方法可以传入自定义线程池,否则就用默认的线程池。
通过以下Java代码建立一个线程池:
线程池创建构造方法传入的几个参数,是根据业务量而定的。根据业务数量,将线程池的参数调整如下:
//定义线程池
public static final ExecutorService threadPool =
new ThreadPoolExecutor(
20,
50,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(50),
new ThreadPoolExecutor.AbortPolicy());
其中常驻线程数量调整到了20,最大线程数量是50,如果超出则采取丢弃策略。
使用CompletableFuture.supplyAsync
开启一个线程。为什么用supplyAsync这个方法呢?因为该方法可以获取返回值。
CompletableFuture创建线程的时候可以传入自定义线程池,否则它就使用默认线程池,刚刚已经创建好线程池了,那就把刚刚创建的线程池传入。
代码如下(threadPool就是刚刚创建好的线程池):
//该方法可以传入自定义线程池,否则就用默认的线程池;
CompletableFuture<Integer> thread = CompletableFuture.supplyAsync(
() -> callRemoteApi(), threadPool);
该线程里只干一件事:就是调用远程API。
上面的线程执行结束后,会返回一个id,然后我们根据该id查询数据库。具体用到的方法是 whenCompleteAsync:
//线程池执行完毕,如果成功了,则回写,如果失败了,就执行失败处理逻辑
thread.whenCompleteAsync(
(result,throwable)-> {
if (result != -10086) //如果线程池返回了调用远程API成功的结果,那么就调用回写数据库的方法
writeBack(result);
else
exceptionHandle(result);//执行失败处理逻辑
}
);
该方法是一个异步回调方法,线程池会回调我们定义好的 【回写数据库】函数。
最后看 whenCompleteAsync 这个方法本身:
方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程
执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)
介绍到这里,理论知识应该没问题了。下面看完整Demo代码:
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.*;
/*
该测试结果完全满足需求。
下面代码的目标:使用线程池调用远程API,远程API会返回一串id
远程API有一定概率返回失败,返回失败的状态码总是 -10086
线程池接收API返回结果,如果成功,就将该id回写数据库
如果失败,就调用异常处理逻辑。
API参考资料:https://www.cnblogs.com/wuwuyong/p/15496841.html
*/
public class E06_CompletableFuture_AsyncCallback {
//定义线程池
//和实施讨论并且根据业务数量,将线程池的参数调整如下:有20个常驻线程。
public static final ExecutorService threadPool =
new ThreadPoolExecutor(
20,
50,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(50),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
//一共需要发送15次远程请求,向数据库回写15次状态
for (int i = 0; i < 15; i++) {
//该方法可以传入自定义线程池,否则就用默认的线程池;
CompletableFuture<Integer> thread = CompletableFuture.supplyAsync(
() -> callRemoteApi(), threadPool);
//线程池执行完毕,如果成功了,则回写,如果失败了,就执行失败处理逻辑
thread.whenCompleteAsync(
(result,throwable)-> {
if (result != -10086) //如果线程池返回了调用远程API成功的结果,那么就调用回写数据库的方法
writeBack(result);
else
exceptionHandle(result);//执行失败处理逻辑
}
);
}
}
//调用一个远程API,该API会返回一个id
private static Integer callRemoteApi(){
try {
//模拟长任务:调用时间。该调用时间比较慢,长达三秒
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Random random = new Random();
int i = random.nextInt(10);
if (judgeSuccess()) {
System.out.println("已经调用远程API接口,此次调用成功,返回的id是:"+i);
return i;
} else {
System.out.println("已经调用远程API接口,此次调用失败");
return -10086;
}
}
private static void writeBack(int id) {
//回写状态到数据库是一个短任务(相对而言)
System.out.println("已经将id为:" + id + "的单据,回写状态到数据库");
}
//如果远程调用失败了就进行异常处理。
private static void exceptionHandle(int code){
System.out.println("本次调用失败,已经调用失败处理逻辑。错误码为:"+code);
}
//调用远程接口有50%的概率失败。这个方法用于判断是否接口调用成功
private static boolean judgeSuccess() {
Random random = new Random();
int i = random.nextInt(10);
if (i <= 5) {
return true;
} else {
return false;
}
}
}
https://zhuanlan.zhihu.com/p/415851066
这只是一个简单的demo。而CompletableFuture最强大的地方在于他能够编排,决定各个线程执行次序,等待线程执行结果等等。可以让复杂的操作利用多个线程完成。详见https://www.cnblogs.com/wuwuyong/p/15496841.html 这篇博客,里面提供了更详尽的例子。