背景
今天在开发质量平台时需要获取某些数据,要请求公司某个工程的OpenAPI接口A。此接口为返回通用数据的接口,且接口本身的RT都在2~3秒之间。使用该接口,需要进行两次循环获取,然后对返回数据进行处理组装,才能得到我这边工程需要的数据。
在最开始的时候,我天真的写了两层循环,外层循环为一星期的每一天,内层循环为选取的几个版本号。结果发现整个请求过程(请求接口B和C获取版本相关数据->两层循环请求接口A->数据过滤筛选->数据组装排序)下来,响应时间来到了恐怖的2分钟(🤔要被领导骂死了)
同时数据又都要实时获取,无法使用定时任务和缓存的方式
解决思路
将for循环改为多线程的方式进行执行,一种常用的方法是使用Executor框架
package com.xxx.xxx;
...
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// 模拟数据库中的100条数据;
List list = new ArrayList();
for (int i = 0; i < 100; i++) {
list.add(i);
}
//Executors创建线程池new固定的10个线程
ExecutorService taskExecutor = Executors.newCachedThreadPool();
final CountDownLatch latch = new CountDownLatch(list.size());//用于判断所有的线程是否结束
System.out.println("个数==" + list.size());
for (int m = 0; m < list.size(); m++) {
final int n = m;//内部类里m不能直接用,所以赋值给n
Runnable run = new Runnable() {
public void run() {
try {
System.out.println("我在执行=" + n);
} finally {
latch.countDown(); //每次调用CountDown(),计数减1
}
}
};
taskExecutor.execute(run);//开启线程执行池中的任务。还有一个方法submit也可以做到,它的功能是提交指定的任务去执行并且返回Future对象,即执行的结果
}
try {
//等待所有线程执行完毕
latch.await();//主程序执行到await()函数会阻塞等待线程的执行,直到计数为0
} catch (InterruptedException e) {
e.printStackTrace();
}
taskExecutor.shutdown();//关闭线程池
//所有线程执行完毕,执行主线程
}
}
复制代码
注意:在使用多线程时,需要注意线程安全问题,如果程序中使用了共享变量,需要进行同步处理。
业务使用
@Override
public List<JSONObject> getBoomCrash(String appId, String androidEventType, String OS, Set<String> appVersionSet, List<Map<String, Long>> timeScope) throws URISyntaxException, IOException {
Map<String, String[]> versionTagMap = new HashMap<>();
// 首先获取版本信息。业务代码,省略
....
// 第一步先获取传入版本所有的crash数据,并过滤掉版本首次出现的。业务代码,省略
List<BoomCrashDataVo> boomCrashDataList = ...
// 第二步,获取所有版本和UV【以昨日数据为标准,结果是UV倒序排列】。业务代码,省略
List<CrashVersionUvDataVo> versionUvResult = ...
// 第三步,判断当前版本的上一个全量版本。业务代码,省略
String lastVersion = ...
List versionList = new ArrayList();
for (String key : appVersionSet) {
versionList.add(key);
}
versionList.add(lastVersion);
String versionListstr = StringUtils.join(versionList, ",");
List<JSONObject> boomCrashDataListNew = new ArrayList<>();
// 第四步,循环判断获取某个issue数据的数量情况
// Executors创建线程池new固定的10个线程
ExecutorService taskExecutor = Executors.newCachedThreadPool();
final CountDownLatch latch = new CountDownLatch(boomCrashDataList.size());//用于判断所有的线程是否结束
for (BoomCrashDataVo boomCrashData : boomCrashDataList) {
Runnable run = new Runnable() {
public void run() {
try {
// 这里是业务代码
...
} finally {
latch.countDown(); //每次调用CountDown(),计数减1
}
}
};
taskExecutor.execute(run);
}
try {
//等待所有线程执行完毕
latch.await(); //主程序执行到await()函数会阻塞等待线程的执行,直到计数为0
} catch (InterruptedException e) {
e.printStackTrace();
}
taskExecutor.shutdown();
// 按照TOP进行正序排序
Collections.sort(boomCrashDataListNew, new Comparator<JSONObject>() {
@Override
public int compare(JSONObject v1, JSONObject v2) {
Integer uv1 = v1.getIntValue("topNumber");
Integer uv2 = v2.getIntValue("topNumber");
return uv1.compareTo(uv2);
}
});
return boomCrashDataListNew;
}
复制代码
改造成果
响应时间降到了20~30秒,和业务沟通在可接受范围内。同时,前端我修改成了在请求数据过程中显示加载组件(参考antd的<Spin />),这样就不会显示太过突兀,提升用户使用体验。
深入学习
执行器服务
java.util.concurrent.ExecutorService 接口表示一个异步执行机制,使我们能够在后台执行任务。因此一个 ExecutorService 很类似于一个线程池。实际上,存在于 java.util.concurrent 包里的 ExecutorService 实现就是一个线程池实现。
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
executorService.shutdown();
复制代码
首先使用 newFixedThreadPool() 工厂方法创建一个 ExecutorService。这里创建了一个十个线程执行任务的线程池。然后,将一个 Runnable 接口的匿名实现类传递给 execute() 方法。这将导致 ExecutorService 中的某个线程执行该 Runnable。
任务委派
下图说明了一个线程是如何将一个任务委托给一个 ExecutorService 去异步执行的:
一旦该线程将任务委派给 ExecutorService,该线程将继续它自己的执行,独立于该任务的执行。
ExecutorService实现
既然 ExecutorService 是个接口,如果你想用它的话就得去使用它的实现类之一。 java.util.concurrent 包提供了 ExecutorService 接口的以下实现类:
- ThreadPoolExecutor
- ScheduledThreadPoolExecutor
ExecutorService使用
有几种不同的方式来将任务委托给 ExecutorService 去执行:
- execute(Runnable)
- submit(Runnable)
- submit(Callable)
- invokeAny(...)
- invokeAll(...)
execute(Runnable)
execute(Runnable) 方法要求一个 java.lang.Runnable 对象,然后对它进行异步执行。以下是使用 ExecutorService 执行一个 Runnable 的示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
executorService.shutdown();
复制代码
没有办法得知被执行的 Runnable 的执行结果。如果有需要的话你得使用一个 Callable(以下将做介绍)。
submit(Runnable)
submit(Runnable) 方法也要求一个 Runnable 实现类,但它返回一个 Future 对象。这个Future 对象可以用来检查 Runnable 是否已经执行完毕。
以下是 ExecutorService submit() 示例:
Future future = executorService.submit(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
future.get(); //returns null if the task has finished correctly
复制代码
submit(Callable)
submit(Callable) 方法类似于 submit(Runnable) 方法,除了它所要求的参数类型之外。Callable 实例除了它的 call() 方法能够返回一个结果之外和一个 Runnable 很相像。
Runnable.run() 不能够返回一个结果。Callable 的结果可以通过 submit(Callable) 方法返回的 Future 对象进行获取。以下是一个
ExecutorService Callable 示例:
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
System.out.println("Asynchronous Callable");
return "Callable Result";
}
});
System.out.println("future.get() = " + future.get());
// 输出
Asynchronous Callable
future.get() = Callable Result
复制代码
invokeAny()
invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个 Future,但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果 - 只能表明其中一个已执行结束。
如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。
以下是示例代码:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown()
复制代码
上述代码将会打印出给定 Callable 集合中的一个的执行结果
invokeAll()
invokeAll() 方法将调用你在集合中传给 ExecutorService 的所有 Callable 对象。invokeAll() 返回一系列的 Future 对象,通过它们你可以获取每个 Callable 的执行结果。
记住,一个任务可能会由于一个异常而结束,因此它可能没有 "成功"。无法通过一个 Future 对象来告知我们是两种结束中的哪一种。
以下是一个代码示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});
List<Future<String>> futures = executorService.invokeAll(callables);
for(Future<String> future : futures){
System.out.println("future.get = " + future.get());
}
executorService.shutdown();
复制代码
ExecutorService关闭
使用完 ExecutorService 之后你应该将其关闭,以使其中的线程不再运行。比如,如果你的应用是通过一个 main() 方法启动的,之后 main 方法退出了你的应用,如果你的应用有一个活动的 ExexutorService 它将还会保持运行。ExecutorService 里的活动线程阻止了 JVM 的关闭。
要终止 ExecutorService 里的线程你需要调用 ExecutorService 的 shutdown() 方法。ExecutorService 并不会立即关闭,但它将不再接受新的任务,而且一旦所有线程都完成了当前任务的时候,ExecutorService 将会关闭。在 shutdown() 被调用之前所有提交给ExecutorService 的任务都被执行。
如果你想要立即关闭 ExecutorService,你可以调用 shutdownNow() 方法。这样会立即尝试停止所有执行中的任务,并忽略掉那些已提交但尚未开始处理的任务。无法担保执行任务的正确执行。可能它们被停止了,也可能已经执行结束。