在执行者中执行返回结果的任务

Executor 框架的一个优点是能运行返回结果的并发任务. Java Concurrent API 提供了以下 2 个接口:

  • Callable : 此接口有 call() 方法. 在此方法中实现任务的逻辑. Callable 接口是一个参数化的接口, 意味着可以表明 call() 方法将返回的数据类型.
  • Future : 此接口有一些获取由 Callable 对象生成的结果并管理其状态的方法

任务

生成多个线程在一个最大大小为2的线程池里执行, 每个线程计算一个数字的阶乘.

实现

本节的示例代码在 com.elanzone.books.noteeg.chpt4.sect04 package中

  • FactorialCalculator 类 : 实现 Callable 接口, 参数为 Integer 类型

    • number 属性: Integer 对象, 保存线程要计算阶乘的数字
    • 在构造函数中初始化上述属性
        private Integer number;
    
        public FactorialCalculator(Integer number) {
            this.number = number;
        }
    
    • call() 方法 : 返回 number 的阶乘数. 为了演示目的,在每次乘法后让任务睡上 20 毫秒
        @Override
        public Integer call() throws Exception {
            int result = 1;
            if ((number == 0) || (number == 1)) {
                result = 1;
            } else {
                for (int i = 2; i <= number; i++) {
                    result *= i;
                    TimeUnit.MILLISECONDS.sleep(20);
                }
            }
            System.out.printf("%s: %d\n", Thread.currentThread().getName(), result);
            return result;
        }
    
  • 控制类 : Main

    • 用 Executors 类的 newFixedThreadPool() 方法创建 ThreadPoolExecutor 以执行任务. 传递 2 作为参数.
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
    
    • 创建一个 Future<Integer> 对象列表
        List<Future<Integer>> resultList = new ArrayList<>();
    
    • 生成 10 个在 0 和 10 之间的随机数并为每个随机数创建一个 FactorialCalculator 交由 executor 执行,将结果加入到 resultList
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            Integer number = random.nextInt(10);
            FactorialCalculator calculator = new FactorialCalculator(number);
            Future<Integer> result = executor.submit(calculator);
            resultList.add(result);
        }
    
    • 循环检查执行者的状态直到所有线程结束执行, 每次检查间隔 50 毫秒
      • 使用 Future 的 isDone() 方法判断任务是否已完成
      • 使用执行者的 getCompletedTaskNumber() 方法获得已完成的任务数
        do {
            System.out.printf("Main: Number of Completed Tasks: %d\n", executor.getCompletedTaskCount());
            for (int i = 0; i < resultList.size(); i++) {
                Future<Integer> result = resultList.get(i);
                System.out.printf("Main: Task %d: %s\n", i, result.isDone());
            }
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while (executor.getCompletedTaskCount() < resultList.size());
    
    • 输出每个任务的计算结果
      • 使用任务返回结果的 get() 方法获得 Integer 对象
        System.out.printf("Main: Results\n");
        for (int i = 0; i < resultList.size(); i++) {
            Future<Integer> result = resultList.get(i);
            Integer number = null;
            try {
                number = result.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            System.out.printf("Main: Task %d: %d\n", i, number);
        }
    
    • 最后调用执行者的 shutdown() 方法结束执行
        executor.shutdown();
    

工作原理

本例演示了如何使用 Callable 接口启动返回结果的并发任务。 FactorialCalculator 类实现了由 Integer 作为结果类型的 Callable 接口。因此它的 call() 方法返回 Future<Integer> 类型。

另一个重点在 Main 类中。使用 submit() 方法将一个 Callable 对象送去一个执行者内执行。 此方法接受一个 Callable 对象作为参数并返回一个 Future 对象,您可以将 Future 对象用于2个主要目的:

  • 控制任务的状态: 能取消任务并检查它是否结束。本例中使用了 isDone() 方法检查任务是否已结束
  • 获得 call() 方法返回的结果: 使用 get() 方法。此方法等待直到 Callable 对象结束 call() 方法的执行并返回结果。
    • 如果线程在 get() 方法等待结果时被中断,它抛出一个 InterruptedException 异常
    • 如果 call() 方法抛出异常,get() 方法抛出一个 ExecutionException 异常

了解更多

当调用一个 Future 对象的 get() 方法而此对象控制的任务尚未结束,此方法阻塞直到任务结束。 Future 接口提供了另一个版本的 get() 方法。

  • get(long timeout, TimeUnit unit): 如果任务结果还没有出来,则等待指定时间。如果指定时间已过而结果尚未出来,此方法返回 null。