今天项目上碰到一个问题,需要调取第三方的数据来处理一些业务需求。因为一次性调用数据返回的json格式的结果无论是响应速度还是解析效率上都是十分低下,为了优化这个功能,于是想到了通过ExecutorService接口及Executors工具类来实现多线程的处理数据。这里分享下本次优化的内容同时也复习下这几个相关的知识点。
Executor工具类
在说ExecutorService前必须先了解下Executor工具类。尽管Java为我们提供了良好的多线程操作方案,在实际情况下使用多线程时我们仍旧会为一些问题所困扰,比如并发原语 (如:synchronized, wait等) 的使用, 对这些语句的使用不当会造成不可侦测的问题,另外太过依赖原语比如synchronized会大幅降低程序的性能。因此我们需要一款强大且易扩展的高性能工具类。Executor就是其中的典范。
Executor声明了一个execute方法,该方法接收Runnable对象作为参数,该方法可以在某一时刻执行传入的线程任务。
尽管Executor提供了诸多方便的功能,但仍有些不足:
首先,Executor仅支持Runnable接口。而Runnable接口的run方法是不支持返回值的。
其次,Executor无法执行一组操作。
最后,Executor没有提供一种合理的方式关闭Executor。
为了解决这些问题可以通过ExecutorService来解决。
ExecutorService接口
ExecutorService接口提供传递Callable作为参数的方法。与Runnable不同的是,Callable可以拥有返回值且可以抛出异常,这也为程序提供了便利。同时,ExecutorService接口也支持通过Future接口接收任务的返回结果。
这里列举几个比较常用的方法:
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>>
调用该方法会执行所有的Callable任务,无论结果是正常返回还是报错亦或是抛出异常,该方法都会返回一个类型为Future的List。
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
调用invokeAny方法之后,只要有一个Callable任务执行完成,且没有抛出异常,未完成的任务都会取消并返回结果。
<T> Future<T> submit(Callable<T> task);
调用该submit方法会执行一个Callable任务并返回Future类型的结果。
项目实例
因为一次请求获取全部的数据,其响应及处理会十分缓慢,这里我们定义了每次获取的数量pageSize,同时为了方便测试这里数据总数totalCount写死为5181。
首先新创建一个ExecutorService实例:
ExecutorService executorService = Executors.newFixedThreadPool(100);
一般来说,Executors支持的线程池大概分为4种:
newFixedThreadPool(int nThreads):创建一个拥有固定线程数量的线程池,在任意时间点上,最多有nThreads个线程处理任务,如果当所有线程都处于工作状态且有新的任务添加进来,新的任务会持续等待直到有线程空闲下来。如果任意线程在关闭前因任何问题导致线程执行结束,新任务会取代该任务继续执行。线程会在线程池中持续存在直到被关闭。
newWorkStealingPool(int parallelism):创建一个拥有足够线程数量的线程池以满足线程的并行执行,并行度取决于当前所有线程数量 (可加入的线程,加入的线程以及正在处理的线程) 的最大值。需要注意的是,线程的真实数量可能变大或者变小。work-stealing 线程池无法保证线程的执行顺序。
newCachedThreadPool(): CachedThreadPool会根据实际情况维护线程数,并且能够重复利用已创建的旧线程。当程序需要执行许多生命周期较短的异步任务时,CachedThreadPool能够很好地提升程序的性能。如果现有的线程不够,该线程池会新创建一个新的线程。如果某一线程超过6秒为被使用,该线程会被关闭并且从线程池中移除。因此,该线程池不会因为持有长空闲的线程而占据任何资源。
newCachedThreadPool(int corePoolSize):创建一个能够定时执行的线程池,该线程池会固定持有corePoolSize个线程。
这里通过计算获取需要执行的Callable任务数量:
int totalPage = 5181;//需要回去的数据数量这里为方便测试写死
int pageSize = 300;//每次获取的数据数量
int loopCount = (totalPage % pageSize) == 0 ? totalPage / pageSize : totalPage / pageSize + 1;//获取需要请求的次数
确定所需要的Callable数量之后,接下来就可以依次创建我们所需要的Callable任务及对应的方法:
Set<Callable<String>> set = new LinkedHashSet<>();
for (int i = 0; i < loopCount; i++) {
int count = i;
Callable<String> callable = new Callable<String>() {//创建callable
@Override
public String call() throws Exception {
return getFullStuInfo(count, pageSize, access_token);
}
};
set.add(callable);//add进set集合
}
任务创建好后就可以调用invokeAll方法并通过List接收方法返回的值。
List<Future<String>> futures = new ArrayList<>();
futures = executorService.invokeAll(set);//同时调用所有callable
获取返回值的List之后就可以循环List处理返回的结果了:
for (Future<String> subFu : futures) {
String result = subFu.get();//获取获得的数据
System.out.println(result);
}
至此结束,最后执行后耗时在2~6s,而如果单线程请求全部数据因为服务器数据读取以及网络问题耗时会在120秒或更多。
下面是完整代码:
public void test() {
ExecutorService executorService = Executors.newFixedThreadPool(100);
try {
long start = System.currentTimeMillis();//测试程序开始时间
String access_token = token();
int totalPage = 5181;//需要回去的数据数量这里为方便测试写死
int pageSize = 300;//每次获取的数据数量
int loopCount = (totalPage % pageSize) == 0 ? totalPage / pageSize : totalPage / pageSize + 1;//获取需要请求的次数
List<Future<String>> futures = new ArrayList<>();
Set<Callable<String>> set = new LinkedHashSet<>();
for (int i = 0; i < loopCount; i++) {
int count = i;
Callable<String> callable = new Callable<String>() {//创建callable
@Override
public String call() throws Exception {
return getFullStuInfo(count, pageSize, access_token);
}
};
set.add(callable);//add进set集合
}
futures = executorService.invokeAll(set);//同时调用所有callable
for (Future<String> subFu : futures) {
String result = subFu.get();//获取获得的数据
System.out.println(result);
}
long end = System.currentTimeMillis();//程序结束时间
System.out.println((end - start) / 1000 + "s");
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();//关闭
}
}
public static String getFullStuInfo(int pageNum, int pageSize, String access_token) {
String param = "";
String result = null;
JSONObject json = null;
JSONArray jsonArray = null;
String baseInfoVersion = "V1.0";
param = "version=" + baseInfoVersion + "&access_token=" + access_token;
String baseInfoUrl = "http://XXXXXXX/XXXX/XXXX/XXXX/XXXXX/" + pageNum + "/" + pageSize + "XXXX";
result = HttpUtils.sendGet(baseInfoUrl, param);
try {
result = URLDecoder.decode(result, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
json = JSONObject.fromObject(result);
jsonArray = json.getJSONArray("result");
String searchIndex = "";
for (int i = 0; i < jsonArray.size(); i++) {
String search = jsonArray.getJSONObject(i).getString("");
searchIndex += searchIndex.equals("") ? search : "#$" + search;
}
return result;
}