Java中有两种异常:
- **受检异常:**这些异常必须指定在方法的throws子句中或者内部抓取。例如,IOException或者ClassNotFoundException。
- **非受检异常:**这些异常不需要被指定或抓取,例如NumberFormatException。
在ForkJoinTask类的compute()方法中不能抛出任何受检异常,因为此方法的实现中不包含任何throws定义,需要加入必要的代码来处理受检异常。另一方面,此方法能够抛出(或者通过此方法内部使用的任何方法或对象抛出)非受检异常。ForkJoinTask和ForkJoinPool类的特性与所期待的不同。程序不会结束执行且不在控制台中看到任何异常信息,只是简单的接受好似异常不会被抛出。只有当调用初始任务的get()方法时,异常才会被抛出。当然,也可以使用ForkJoinTask类的一些方法去辨别任务是或否已经抛出异常,如果是,知道抛出什么类型的异常。在本节中,讲学习如何获取这些信息。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤实现范例:
-
创建名为Task的类,继承Integer类参数化的RecursiveTask类:
public class Task extends RecursiveTask<Integer> {
-
定义名为array的私有int数组,用来模拟范例中将要处理的数据数组:
private int array[];
-
定义两个名为start和end的私有int属性,用来确定任务需要处理的数组元素:
private int start, end;
-
实现类构造函数,初始化这些属性:
public Task(int[] array, int start, int end) { this.array = array; this.start = start; this.end = end; }
-
实现任务的compute()方法,因为已经使用Integer类初始化RecursiveTask类,此方法返回Integer对象。首先,输出start和end属性值的信息到控制台:
@Override protected Integer compute() { System.out.printf("Task: Start from %d to %d\n",start,end);
-
如果任务需要处理的元素集合,由start和end属性确定的范围小于10,则判断数组中第四个元素(索引数是3)是否在集合中。如果是,抛出RuntimeException。然后,设置任务休眠一秒钟:
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(); }
-
否则(任务需要处理的元素集合数量大于等于10),将元素集合拆分成两块,创建两个Task对象来处理它们,并且在线程池中使用invokeAll()方法执行。然后输出这些任务的结果到控制台:
} 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: Result form %d to %d: %d\n",start,mid,task1.join()); System.out.printf("Task: Result form %d to %d: %d\n",mid,end,task2.join()); }
-
输出指明任务结束的信息到控制台,包括start和end属性值:
System.out.printf("Task: End form %d to %d\n",start,end);
-
返回数值0作为任务结果:
return 0; }
-
实现范例的主方法,创建一个包含main()方法的Main类:
public class Main { public static void main(String[] args) {
-
创建100个整型数组:
int array[] = new int[100];
-
创建Task对象处理这个数组:
Task task = new Task(array, 0, 100);
-
使用默认构造函数创建ForkJoinPool对象:
ForkJoinPool pool = new ForkJoinPool();
-
在线程池中使用execute()方法执行任务:
pool.execute(task);
-
使用shutdown()方法关闭ForkJoinPool类:
pool.shutdown();
-
使用awaitTermination()方法等待任务结束。因为需要足够长的时间等待任务结束,将数值1和TimeUnit.DAYS作为参数传给此方法:
try{ pool.awaitTermination(1, TimeUnit.DAYS); } catch (InterruptedException e) { e.printStackTrace(); }
-
使用isCompletedAbnormally()判断任务或者其子任务是否已经抛出异常。如果是,输出抛出的异常信息到控制台。使用ForkJoinTask类的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,如果是,拆分成两部分,并且创建两个新的Task对象分别处理它们。否则寻找数组中的第四个元素(索引数是3),如果此元素在任务处理的集合中,抛出RuntimeException异常。
当执行范例时抛出了异常,但程序不会停止。在Main类中,包含使用初始任务调用ForkJoinTask类的isCompletedAbnormally()方法。如果任务或其子任务已经抛出异常,方法返回true。还用到了同一个对象的getException()方法得到任务抛出的Exception对象。
当在任务中抛出非受检异常时,也会影响到父任务(发送到ForkJoinPool类的任务)以及父任务的父任务,以此类推。如果查看范例的全部输出,就会发现缺失一些任务结束的输出信息。如下所示这些任务的开始信息:
Task: Start from 0 to 100
Task: Start from 0 to 50
Task: Start from 50 to 100
Task: Start from 0 to 25
Task: Start from 50 to 75
Task: Start from 25 to 50
Task: Start from 62 to 75
这些是抛出异常的任务及其父任务,都已经非正常结束。考虑到在开发过程中,如果不想要这种特性的话,使用能够抛出异常的ForkJoinPool和ForkJoinTask对象。
下图显示本范例在控制台输出的部分执行信息:
扩展学习
本范例中,用到join()方法等待任务结束,且得到任务结果。也可以使用如下两种形式的get()方法达到此目的:
- get():如果ForkJoinTask已经结束执行,此方法返回compute()方法返回的值,或者等待直到任务结束。
- get(long timeout, TimeUnit unit):在这个方法中,如果任务的结果无效,则等待指定的时间,如果已过指定时间且结果依然无效,此方法返回null值。TimeUnit是一个枚举类型的类,包含如下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS、和SECONDS。
get()和join()方法有两个主要的不同点:
- join()方法不能被中断,如果中断调用join()方法的线程,此方法抛出InterruptedException异常。
- 如果任务抛出任何非受检异常,get()方法将返回ExecutionException异常,然而join()方法将返回RuntimeException异常。
如果本范例中使用ForkJoinTask类的completeExceptionally()方法代替抛出异常,能够得到相同的结果。代码如下所示:
Exception e=new Exception("This task throws an Exception: "+"Task from "+start+" to "+end);
completeExceptionally(e);
更多关注
- 本章“创建fork/join池”小节