在任务中抛出异常

Java中有2种类型的异常:

  • 受检查(Checked)的异常:必须在方法的throws语句中指明或在方法内捕获。例如 IOException 或 ClassNotFoundException
  • 不受检查(Unchecked)的异常:不必指明或捕获。例如:NumberFormatException

不能在 ForkJoinTask 的 compute() 方法中抛出受检查异常,因为此方法的实现不包含 throws 声明。 必须包含必要的代码来处理异常。另一方面,(它或此方法内使用的任意方法或对象)可以抛出不受检查的异常。 ForkJoinTask 和 ForkJoinPool 类的行为和您可能预期的不一样。程序不会结束执行,您也不会看到终端有关于异常的任何信息。 异常被简单地忽略,就像没有异常被抛出一样。 然而可以使用 ForkJoinTask 类的某些方法来获知一个任务是否抛出了异常以及抛出了什么类型的异常。

任务

如果任务非正常结束,获知导致非正常退出的异常信息。

实现

本节的示例代码在 com.elanzone.books.noteeg.chpt5.sect05 package中

  • 任务类 : Task : extends RecursiveTask<Integer>

    • 序列化版本UID : serialVersionUID
    • 属性:int数组(模拟将要处理的数据数组)及起始、结束值
    • 构造函数将各属性初始化为参数值
            private static final long serialVersionUID = 1L;
    
            private int array[];
            private int start, end;
    
            public Task(int[] array, int start, int end) {
                this.array = array;
                this.start = start;
                this.end = end;
            }
    
    • compute() 方法 :因为 RecursiveTask 泛型参数为 Integer,故此方法的返回值为 Integer 对象
      • 输出开始信息
      • 如果任务由 start 和 end 决定的要处理的元素块的大小小于10,检查第4个元素是否在这个块里
        • 如果在,则抛出一个 RuntimeException 异常
        • 睡上1秒钟
      • 否则将元素块均分为2个,创建2个 Task 对象来处理那些块,并用 invokeAll() 方法在池子中执行它们
      • 输出结束信息并返回0
            @Override
            protected Integer compute() {
                System.out.printf("Task: Start from %d to %d\n", start, end);
    
                if (end - start < 10) {
                    if ((3 > start) && (3 < end)) {
                        throw new RuntimeException("This task throws an Exception: Task from " + start + " to " + end);
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    int mid = (end + start) / 2;
                    Task task1 = new Task(array, start, mid);
                    Task task2 = new Task(array, mid, end);
                    invokeAll(task1, task2);
                }
                System.out.printf("Task: End form %d to %d\n", start, end);
                return 0;
            }
    
  • 控制类 : Main

    • 创建有100个整数的数组
    • 创建一个 Task 对象来处理此数组
    • 用默认构造函数创建一个 ForkJoinPool 对象
    • 用 execute() 方法在池子中执行任务
    • 用 shutdown() 方法停止池子
    • 用 awaitTermination() 方法传一个较长的时间作为参数,等待任务执行完
            int array[] = new int[100];
    
            Task task = new Task(array, 0, 100);
            ForkJoinPool pool = new ForkJoinPool();
    
            pool.execute(task);
            pool.shutdown();
            try {
                pool.awaitTermination(1, TimeUnit.DAYS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
    • 用 isCompletedAbnormally() 方法检查任务或子任务是否抛出了异常
      • 如果有抛出,则用 ForkJoinPool 的 getException() 方法获得抛出的异常并将其输出到终端
            if (task.isCompletedAbnormally()) {
                System.out.printf("Main: An exception has ocurred\n");
                System.out.printf("Main: %s\n", task.getException());
            }
            System.out.printf("Main: Result: %d", task.join());
    

讲解

Task 类处理一个数字数组。它检查它要处理的数字块是否有10或更多个元素。 如果是,则将数字块分为2个并对应创建2个新的 Task 对象来处理。 否则看数组的第4个元素是否在要处理的数字块中,如果在,则抛出 RuntimeException 异常。

程序执行、异常被抛出时,但是程序并没有停止。 在 Main 类中调用了原始任务的 ForkJoinTask 类的 isCompletedAbnormally() 方法。 此方法在任务或其中一个子任务抛出过异常时返回 true。 您也使用了 getException() 方法来获知它所抛出的 Exception 对象。

当在一个任务内抛出一个不受检查的异常,它也影响它的父任务(将其送到 ForkJoinPool 类的任务)及祖父任务,等等。 如果您清查程序的所有输出,您将看到没有某些任务的结束时的输出信息。这些任务的开始信息如下:

Task: Starting form 0 to 100
Task: Starting form 0 to 50
Task: Starting form 0 to 25
Task: Starting form 0 to 12
Task: Starting form 0 to 6

这些任务是抛出异常的任务及它的父任务们。它们都没有正常结束。 当您用可能抛出异常的 ForkJoinPool 和 ForkJoinTask 对象而您不期望有这样的行为时,要重视这点。

了解更多

用 ForkJoinTask 类的 completeExceptionally() 方法,而不是抛出异常,也会得到同样的结果。 代码可能类似下面这样:

Exception e = new Exception("This task throws an Exception: Task from "+start+" to "+end);
completeExceptionally(e);