背景

今天在开发质量平台时需要获取某些数据,要请求公司某个工程的OpenAPI接口A。此接口为返回通用数据的接口,且接口本身的RT都在2~3秒之间。使用该接口,需要进行两次循环获取,然后对返回数据进行处理组装,才能得到我这边工程需要的数据。

在最开始的时候,我天真的写了两层循环,外层循环为一星期的每一天,内层循环为选取的几个版本号。结果发现整个请求过程(请求接口B和C获取版本相关数据->两层循环请求接口A->数据过滤筛选->数据组装排序)下来,响应时间来到了恐怖的2分钟(🤔要被领导骂死了)

Java for循环改为多线程 java多线程处理for循环_Java for循环改为多线程

同时数据又都要实时获取,无法使用定时任务和缓存的方式

Java for循环改为多线程 java多线程处理for循环_java_02

解决思路

将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 for循环改为多线程 java多线程处理for循环_jvm_03

深入学习

执行器服务

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 去异步执行的:

Java for循环改为多线程 java多线程处理for循环_开发语言_04

一旦该线程将任务委派给 ExecutorService,该线程将继续它自己的执行,独立于该任务的执行。

ExecutorService实现

既然 ExecutorService 是个接口,如果你想用它的话就得去使用它的实现类之一。 java.util.concurrent 包提供了 ExecutorService 接口的以下实现类:

  1. ThreadPoolExecutor
  2. ScheduledThreadPoolExecutor

ExecutorService使用

有几种不同的方式来将任务委托给 ExecutorService 去执行:

  1. execute(Runnable)
  2. submit(Runnable)
  3. submit(Callable)
  4. invokeAny(...)
  5. 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() 方法。这样会立即尝试停止所有执行中的任务,并忽略掉那些已提交但尚未开始处理的任务。无法担保执行任务的正确执行。可能它们被停止了,也可能已经执行结束。