最近使用多线程优化了一个非常耗时的ping任务,下面的是未优化的源代码,大致就是遍历es取出的list,然后循环判断是否能ping通:
SearchResponse searchResponse = client.search(searchRequest);
Iterator it = searchResponse.getHits().iterator();
while (it.hasNext()) {
boolean isReachAble = false;
SearchHit hit = (SearchHit) it.next();
Link link = new Link();
Map<String, Object> map = hit.getSourceAsMap();
String area = (String) map.get("area");
if ("上海".equals(area)) {
//过滤掉上海地区
continue;
}
List<String> getways = (List<String>) map.get("gateway");
//一旦能连通其中任一网关则代表连接成功
for (String getway : getways) {
//这个isIpReachable很耗时
if (isIpReachable(getway)) {
isReachAble = true;
break;
}
}
link.setArea(area);
link.setIsLink(isReachAble ? 1 : 0);
links.add(link);
}
多线程的话现在基本都是直接使用线程池了吧,如下面第一行代码就能创建一个线程数为4的线程池:
ExecutorService executorService = Executors.newFixedThreadPool(4);
CompletionService<String> pool = new ExecutorCompletionService<String>(executorService);
第二行的这个CompletionService是我们今天介绍的重点,它与默认的ExecutorService的最大区别就是:
通过executorService来submit的task不一定是按照加入自己维护的list顺序完成的;从list中遍历的每个Future对象并不一定处于完成状态,这时调用get()方法就会被阻塞住,如果系统是设计成每个线程完成后就能根据其结果继续做后面的事,这样对于处于list后面的但是先完成的线程就会增加了额外的等待时间。
而CompletionService的实现是维护一个保存Future对象的BlockingQueue。只有当这个Future对象状态是结束的时候,才会加入到这个Queue中,take()方法其实就是Producer-Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象加入到Queue中。
所以,先完成的必定先被取出。这样就减少了不必要的等待时间
好了废话不多说,直接上优化后的代码:
//response中包含从es中取到的数据
SearchResponse searchResponse = client.search(searchRequest);
Iterator it = searchResponse.getHits().iterator();
//创建固定数目线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
CompletionService<String> pool = new ExecutorCompletionService<String>(executorService);
List<Future<String>> resultList = new ArrayList<>();
while (it.hasNext()) {
SearchHit hit = (SearchHit) it.next();
Map<String, Object> map = hit.getSourceAsMap();
List<String> getways = (List<String>) map.get("gateway");
String area = (String) map.get("area");
if ("上海".equals(area)) {
//过滤掉上海地区
continue;
}
//将耗时任务submit到线程池中
resultList.add(pool.submit(() -> {
long t1 = System.currentTimeMillis();
boolean isReachAble = false;
Link link = new Link();
//一旦能连通其中任一网关则代表连接成功
for (String getway : getways) {
if (isIpReachable(getway)) {
isReachAble = true;
break;
}
}
link.setArea((String) map.get("area"));
link.setIsLink(isReachAble ? 1 : 0);
links.add(link);
long t2 = System.currentTimeMillis();
return "task " + map.get("area") + " completed.耗时:" + (t2 - t1);
}));
}
//如果没有下面的代码,主线程将直接返回
for(int i = 0; i < resultList.size(); i++){
//在取到数据之前将会一直阻塞
String result = pool.take().get();
System.out.println(result);
}
我特地将单线程和多线程运行结果做了个对比,可以看到多线程优化过后时间减了一半之多
下面的是控制台输出,发现执行顺序确实基本是按时间由短到长,正好体现出了CompletionService的优点