JDK线程池CompletionService的使用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gui66497/article/details/81633959

最近使用多线程优化了一个非常耗时的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的优点

猜你喜欢

转载自blog.csdn.net/gui66497/article/details/81633959