1. 概述
1.1 Executor
是JDK1.5时引入的,引入该接口的主要目的是解耦任务本身和任务的执行。我们之前通过线程执行一个任务时,往往需要先创建一个线程,然后调用线程的start
方法来执行任务。而Executor接口解耦了任务和任务的执行,该接口只有一个方法,入参为待执行的任务
public interface Executor {
/**
* 执行给定的Runnable任务.
* 根据Executor的实现不同, 具体执行方式也不相同.
void execute(Runnable command);
}
然后有几个实现类:
- 同步执行任务:DirectExecutor,对于传入的任务,只有执行完成后execute才会返回
- 异步执行任务:ThreadPerTaskExecutor ,对于每个任务,执行器都会创建一个新的线程去执行任务。
- 对任务进行排队执行: SerialExecutor,会对传入的任务进行排队(FIFO顺序),然后从队首取出一个任务执行
1.2 ExecutorService
Executor接口提供的功能很简单,为了对它进行增强,出现了ExecutorService接口,ExecutorService继承了Executor,它在Executor的基础上增强了对任务的控制,同时包括对自身生命周期的管理,主要有四类:
- 关闭执行器,禁止任务的提交;
- 监视执行器的状态;
- 提供对异步任务的支持;
- 提供对批处理任务的支持。
对于Future,Future对象提供了对任务异步执行的支持,也就是说调用线程无需等待任务执行完成,提交待执行的任务后,就会立即返回往下执行。然后,可以在需要时检查Future是否有结果了,如果任务已执行完毕,通过Future.get()方法可以获取到执行结果——Future.get()是阻塞方法。
1.3 ScheduledExecutorService
ScheduledExecutorService提供了一系列schedule方法,可以在给定的延迟后执行提交的任务,或者每个指定的周期执行一次提交的任务,该接口继承了ExecutorService
2. 实现类ThreadPoolExecutor
2.1 线程池作用
ThreadPoolExecutor是用来创建线程池的Executor,线程池概念与数据库连接池类似。
当有任务需要执行时,线程池会给该任务分配线程,如果当前没有可用线程,一般会将任务放进一个队列中,当有线程可用时,再从队列中取出任务并执行
线程池的引入,主要解决以下问题:
- 减少系统因为频繁创建和销毁线程所带来的开销;
- 自动管理线程,对使用方透明,使其可以专注于任务的构建。
Executors工厂可以创建不同类型的线程池,其中有以下几个参数:
maximumPoolSize限定了整个线程池的大小,corePoolSize限定了核心线程池的大小,corePoolSize≤maximumPoolSize(当相等时表示为固定线程池);maximumPoolSize-corePoolSize表示非核心线程池。
2.2 线程池状态
ThreadPoolExecutor一共定义了5种线程池状态
- RUNNING : 接受新任务, 且处理已经进入阻塞队列的任务
- SHUTDOWN : 不接受新任务, 但处理已经进入阻塞队列的任务
- STOP : 不接受新任务, 且不处理已经进入阻塞队列的任务, 同时中断正在运行的任务
- TIDYING : 所有任务都已终止, 工作线程数为0, 线程转化为TIDYING状态并准备调用terminated方法
- TERMINATED : terminated方法已经执行完成
各个状态之间的流转图:
2.3 Worker工作线程
概要说明:
当我们向 ThreadPoolExecutor 提交任务时,会发生以下几件事:
1. 判断如果当前线程池中的工作线程数还没有达到 corePoolSize
的值,则调用addWorker方法
2. addWorker方法中首先判断线程池的状态是否允许创建新的Worker对象,其次对比当前线程池中的工作线程数和maximumPoolSize
等参数,判断是否允许创建新的Worker对象(因为创建Worker对象也意味着需要创建工作线程)
3. 条件都通过的情况下,创建Worker对象,而Worker对象的构造器中有且仅有一个参数,将任务作为参数,并会通过getThreadFactory().newThread(this)
方法创建工作线程,即所谓的将任务封装为Worker对象
并将新创建的 Worker
对象添加到 workers
集合中
4. 如果添加成功,则调用Thread的 start
方法(),而由于这个Thread对象是直接获取的Worker对象中的Thread对象赋值而来,所以其实也就是调用了Worker对象中的run方法,该run方法中又调用了runWorker方法,在runWorker方法中去调用了真实任务。
5. 在runWorker方法中
1. 获取当前线程对应的Worker对象和它需要执行的任务task。(入参是一个Worker对象)
2. 将Worker对象的firstTask字段设置为null,以便它可以接收新的任务
3.进入死循环,如果当前线程有任务task或者可以从任务队列(BlockingQueue)中获取到任务(getTask方法),就执行任务
4. 如果循环结束是因为当前线程收到了中断信号,并且线程池的状态为STOP或以上状态,那么将当前线程重新标记为中断状态,以便其他线程可以收到这个中断信号。
5. 最后调用processWorkerExit方法,将当前线程对应的Worker对象从线程池中移除,并做一些清理工作。
所以任务会在两个地方存在1. Worker对象中自带任务 2. 任务队列,而工作线程只存在于Worker对象中,且Worker对象是被放在名为workers集合中,由线程池决定是否摧毁这些工作线程,即将Worker对象从workers集合中删除,并对移除的Worker对象中的线程调用interrupt()方法
以下作简要说明:
2.3.1 工作线程的创建
execute方法内部调用了addWorker方法来添加工作线程并执行任务,整个addWorker的逻辑并不复杂,分为两部分:
第一部分是一个自旋操作,主要是对线程池的状态进行一些判断,如果状态不适合接受新任务,或者工作线程数超出了限制,则直接返回false。
第二部分才真正去创建工作线程并执行任务:首先将Runnable任务包装成一个Worker对象,然后加入到一个任务集合中(名为workers的HashSet),最后调用工作线程中的Thread对象的start方法执行任务,其实最终是委托到Worker的下面方法执行:
public void run() {
runWorker(this);
}
2.3.2 工作线程的执行
runWoker用于执行任务,整体流程如下:
- while循环不断地通过
getTask()
方法从队列中获取任务(如果工作线程自身携带着任务,则执行携带的任务); - 控制执行线程的中断状态,保证如果线程池正在停止,则线程必须是中断状态,否则线程必须不是中断状态;
- 调用
task.run()
执行任务; - 处理工作线程的退出工作。
该方法确保正在停止的线程池(STOP/TIDYING/TERMINATED)不再接受新任务,如果有新任务那么该任务的工作线程一定是中断状态;确保正常状态的线程池(RUNNING/SHUTDOWN),其所执行的任务都是不能被中断的。
另外,getTask方法用于从任务队列中获取一个任务,如果获取不到任务,会跳出while循环,最终会通过processWorkerExit方法清理工作线程。
2.3.3 工作线程的清理
processWorkerExit的作用就是将该退出的工作线程清理掉,然后看下线程池是否需要终止。processWorkerExit执行完之后,整个工作线程的生命周期也结束了,我们可以通过下图来回顾下它的整个生命周期:
2.4 线程池的调度流程
ExecutorService的核心方法是submit方法——用于提交一个待执行的任务.execute的执行流程可以用下图描述
execute的整个执行流程关键是下面两点:
- 如果工作线程数小于核心线程池上限(CorePoolSize),则直接新建一个工作线程并执行任务;
- 如果工作线程数大于等于CorePoolSize,则尝试将任务加入到队列等待以后执行。如果加入队列失败了(比如队列已满的情况),则在总线程池未满的情况下(
CorePoolSize ≤ 工作线程数 < maximumPoolSize
)新建一个工作线程立即执行任务,否则执行拒绝策略。
2.5 任务队列
阻塞队列就是在我们构建ThreadPoolExecutor对象时,在构造器中指定的。由于队列是外部指定的,所以根据阻塞队列的特性不同,Worker工作线程调用getTask方法获取任务的执行情况也不同
1.直接提交
即直接将任务提交给等待的工作线程,这时可以选择SynchronousQueue。因为SynchronousQueue是没有容量的,而且采用了无锁算法,所以性能较好,但是每个入队操作都要等待一个出队操作,反之亦然。
使用SynchronousQueue时,当核心线程池满了以后,如果不存在空闲的工作线程,则试图把任务加入队列将立即失败(execute方法中使用了队列的offer方法进行入队操作,而SynchronousQueue在调用offer时如果没有另一个线程等待出队操作,则会立即返回false),因此会构造一个新的工作线程(未超出最大线程池容量时)。
由于,核心线程池是很容易满的,所以当使用SynchronousQueue时,一般需要将maximumPoolSizes
设置得比较大,否则入队很容易失败,最终导致执行拒绝策略,这也是为什么Executors工作默认提供的缓存线程池使用SynchronousQueue作为任务队列的原因。
2.无界任务队列
无界任务队列我们的选择主要有LinkedTransferQueue、LinkedBlockingQueue(近似无界,构造时不指定容量即可),从性能角度来说LinkedTransferQueue采用了无锁算法,高并发环境下性能相对更好,但如果只是做任务队列使用相差并不大。
使用无界队列需要特别注意系统资源的消耗情况,因为当核心线程池满了以后,会首先尝试将任务放入队列,由于是无界队列所以几乎一定会成功,那么系统瓶颈其实就是硬件了。如果任务的创建速度远快于工作线程处理任务的速度,那么最终会导致系统资源耗尽。Executors工厂中创建固定线程池的方法内部就是用了LinkedBlockingQueue。
3.有界任务队列
有界任务队列,比如ArrayBlockingQueue ,可以防止资源耗尽的情况。当核心线程池满了以后,如果队列也满了,则会创建归属于非核心线程池的工作线程,如果非核心线程池也满了 ,才会执行拒绝策略。
2.6 拒绝策略
ThreadPoolExecutor在以下两种情况下会执行拒绝策略:
- 当核心线程池满了以后,如果任务队列也满了,首先判断非核心线程池有没满,没有满就创建一个工作线程(归属非核心线程池), 否则就会执行拒绝策略;
- 提交任务时,ThreadPoolExecutor已经关闭了。
所谓拒绝策略,就是在构造ThreadPoolExecutor时,传入的RejectedExecutionHandler对象
ThreadPoolExecutor一共提供了4种拒绝策略:
- AbortPolicy(默认):抛出一个RejectedExecutionException异常
- DiscardPolicy:无为而治,什么都不做,等任务自己被回收
- DiscardOldestPolicy:丢弃任务队列中的最近一个任务,并执行当前任务
- CallerRunsPolicy:以自身线程来执行任务,这样可以减缓新任务提交的速度
2.7 线程池的关闭
ExecutorService接口提供两种方法来关闭线程池,这两种方法的区别主要在于是否会继续处理已经添加到任务队列中的任务。
- shutdown方法将线程池切换到SHUTDOWN状态(如果已经停止,则不用切换),并调用interruptIdleWorkers方法中断所有空闲的工作线程,最后调用tryTerminate尝试结束线程池,注意,如果执行Runnable任务的线程本身不响应中断,那么也就没有办法终止任务。
- shutdownNow方法的主要不同之处就是,它会将线程池的状态至少置为STOP,同时中断所有工作线程(无论该线程是空闲还是运行中),同时返回任务队列中的所有任务。
2.8 配置核心线程池的大小
- 如果任务是 CPU 密集型(需要进行大量计算、处理,比如计算圆周率、对视频进行高清解码等等),则应该配置尽量少的线程,比如 CPU 个数 + 1,这样可以避免出现每个线程都需要使用很长时间但是有太多线程争抢资源的情况;
- 如果任务是 IO密集型(主要时间都在 I/O,即网络、磁盘IO,CPU 空闲时间比较多),则应该配置多一些线程,比如 CPU 数的两倍,这样可以更高地压榨 CPU。
公式:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。
3 固定线程池
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
int temp = i;
newFixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ",i:" + temp);
}
});
}
4 单线程线程池
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
类似:newSingleThreadScheduledExecutor()
5 可缓存的线程池
CachedThreadPool
是一个可缓存的线程池,它的作用是在需要时创建新的线程来执行任务,如果有空闲线程则会重用已有线程,如果所有线程都在执行任务,它会创建一个新的线程来处理新的任务。这种线程池适用于任务数比较多,但每个任务的执行时间比较短的情况。因为线程数可以根据任务数动态调整,所以它的资源利用率比较高。
CachedThreadPool
线程池在没有任务需要执行时,会将多余的线程摧毁,从而释放资源。具体的策略是当一个线程空闲时间超过 60s
时,它就会被回收销毁。这是因为在没有任务执行时,多余的线程只会占用系统资源,而不会带来任何的好处。因此,CachedThreadPool
采用了动态调整线程数的策略,当任务执行完毕后,它会根据当前线程池中的线程数量来决定是否要摧毁多余的线程。这样可以保证在任务比较密集时,线程池中有足够的线程可用来执行任务,而在任务比较稀疏时,又能释放多余的资源,避免资源浪费。
需要注意的是,当线程被摧毁后,如果有新的任务到来,线程池需要再次创建新的线程来执行任务。这样可能会带来一定的性能损失,因此在使用 CachedThreadPool
时,需要根据具体的应用场景来选择合适的线程池。
// 1.可缓存的线程池 重复利用
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
int temp = i;
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("threadName:" + Thread.currentThread().getName() +
",i:" + temp);
}
});
}
6 可延时/周期调度的线程池
ScheduledThreadPoolExecutor,它是对普通线程池ThreadPoolExecutor的扩展,增加了延时调度、周期调度任务的功能。概括下ScheduledThreadPoolExecutor的主要特点:
- 对Runnable任务进行包装,封装成
ScheduledFutureTask
,该类任务支持任务的周期执行、延迟执行; - 采用
DelayedWorkQueue
作为任务队列。该队列是无界队列,所以任务一定能添加成功,但是当工作线程尝试从队列取任务执行时,只有最先到期的任务会出队,如果没有任务或者队首任务未到期,则工作线程会阻塞; ScheduledThreadPoolExecutor
的任务调度流程与ThreadPoolExecutor略有区别,最大的区别就是,先往队列添加任务,然后创建工作线程执行任务。
创建一个定长线程池,支持定时及周期性任务执行。
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 10; i++) {
int temp = i;
newScheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ",i:" + temp);
}
},3, TimeUnit.SECONDS);
}
CachedThreadPool
和固定线程池的区别主要在于线程数的处理方式。
CachedThreadPool
的线程数是根据任务数量动态调整的,当有新任务需要执行时,如果当前没有可用的线程,则会创建一个新的线程。而如果有空闲线程,则会重用已有线程来执行任务。因此,CachedThreadPool
的线程数是不固定的,它会根据任务的需求来动态调整线程数,最大线程数可以达到 Integer.MAX_VALUE
。
相比之下,固定线程池中的线程数是固定的,无论是否有任务需要执行,线程数都是不变的。如果任务数量超过线程数,那么多余的任务就会被放入队列中等待执行。固定线程池适合于长时间运行的任务,它可以避免线程频繁的创建和销毁,从而提高了性能。
因此,CachedThreadPool
和固定线程池适用于不同的场景。CachedThreadPool
适用于任务数比较多但是每个任务的执行时间比较短的情况,而固定线程池适用于任务数比较少但是每个任务的执行时间比较长的情况。
7. Future模式
7.1 简介
Future模式是Java多线程设计模式中的一种常见模式,它的主要作用就是异步地执行任务,并在需要的时候获取结果。我们知道,一般调用一个函数,需要等待函数执行完成,调用线程才会继续往下执行,如果是一些计算密集型任务,需要等待的时间可能就会比较长。
Future模式可以让调用方立即返回,然后它自己会在后面慢慢处理,此时调用者拿到的仅仅是一个凭证,调用者可以先去处理其它任务,在真正需要用到调用结果的场合,再使用凭证去获取调用结果。这个凭证就是这里的Future。
我们看下时序图来理解下两者的区别:
传统的数据获取方式:
Future模式下的数据获取:
7.2 并发包中Future模式中的各个组件
7.2.1 真实的任务类
首先我们需要类可以返回线程的执行结果,而传统实现Runnable接口的线程是获取不了返回值的
于是,JDK提供了另一个接口——Callable
,表示一个具有返回结果的任务:
public interface Callable<V> {
V call() throws Exception;
}
所以,最终我们自定义的任务类一般都是实现了Callable接口。以下定义了一个具有复杂计算过程的任务,最终返回一个Double值:
public class ComplexTask implements Callable<Double> {
@Override
public Double call() {
// complex calculating...
return ThreadLocalRandom.current().nextDouble();
}
}
7.2.2 凭证
Future模式可以让调用方获取任务的一个凭证,以便将来拿着凭证去获取任务结果,凭证需要具有以下特点:
- 在将来某个时间点,可以通过凭证获取任务的结果;
- 可以支持取消。
并发包中提供了Future接口和它的实现类——FutureTask
来满足我们的需求
所以我们可以将上面的代码改造成:
ComplexTask task = new ComplexTask();
Future<Double> future = new FutureTask<Double>(task);
上面的FutureTask就是真实的“凭证”,Future则是该凭证的接口(从面向对象的角度来讲,调用方应面向接口操作)。
那既然要执行任务,FutureTask这个类其实除了实现了Future凭证接口外,还实现了Runable接口
FutureTask既可以包装Callable任务,也可以包装Runnable任务,但最终都是将Runnable转换成Callable任务,其实是一个适配过程。
最终,调用方可以以下面这种方式使用Future模式,异步地获取任务的执行结果。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ComplexTask task = new ComplexTask();
Future<Double> future = new FutureTask<Double>(task);
// time passed...
Double result = future.get();
}
通过上面的分析,可以看到,整个Future模式其实就三个核心组件:
- 真实任务/数据类(通常任务执行比较慢,或数据构造需要较长时间),即示例中的ComplexTask
- Future接口(调用方使用该凭证获取真实任务/数据的结果),即Future接口
- Future实现类(用于对真实任务/数据进行包装),即FutureTask实现类
注意:
- FutureTask虽然支持任务的取消(cancel方法),但是只有当任务是初始化(NEW状态)时才有效,否则cancel方法直接返回false;
- 当执行任务时(run方法),无论成功或异常,都会先过渡到COMPLETING状态,直到任务结果设置完成后,才会进入响应的终态。
7.3 FutureTask
既然是任务,就有状态,FutureTask一共给任务定义了7种状态:
- NEW:表示任务的初始化状态;
- COMPLETING:表示任务已执行完成(正常完成或异常完成),但任务结果或异常原因还未设置完成,属于中间状态;
- NORMAL:表示任务已经执行完成(正常完成),且任务结果已设置完成,属于最终状态;
- EXCEPTIONAL:表示任务已经执行完成(异常完成),且任务异常已设置完成,属于最终状态;
- CANCELLED:表示任务还没开始执行就被取消(非中断方式),属于最终状态;
- INTERRUPTING:表示任务还没开始执行就被取消(中断方式),正式被中断前的过渡状态,属于中间状态;
- INTERRUPTED:表示任务还没开始执行就被取消(中断方式),且已被中断,属于最终状态。
7.3 结果获取
FutureTask可以通过get方法获取任务结果,如果需要限时等待,可以调用get(long timeout, TimeUnit unit)
。如果当前任务的状态是NEW或COMPLETING,会调用awaitDone
阻塞线程。否则会认为任务已经完成,直接通过report
方法映射结果
7.4 ScheduledFutureTask
ScheduledFutureTask是ScheduledThreadPoolExecutor这个线程池的默认调度任务类。
ScheduledFutureTask在普通FutureTask的基础上增加了周期执行/延迟执行的功能
8. Fork/Join框架
8.1 分治思想
算法领域有一种基本思想叫做“分治”,所谓“分治”就是将一个难以直接解决的大问题,分割成一些规模较小的子问题,以便各个击破,分而治之。
比如:对于一个规模为N的问题,若该问题可以容易地解决,则直接解决;否则将其分解为K个规模较小的子问题,这些子问题互相独立且与原问题性质相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解,这种算法设计策略叫做分治法。
许多基础算法都运用了“分治”的思想,比如二分查找、快速排序等等。
基于“分治”的思想,J.U.C在JDK1.7时引入了一套Fork/Join框架。Fork/Join框架的基本思想就是将一个大任务分解(Fork)成一系列子任务,子任务可以继续往下分解,当多个不同的子任务都执行完成后,可以将它们各自的结果合并(Join)成一个大结果,最终合并成大任务的结果:
8.2 工作窃取算法
从上述Fork/Join框架的描述可以看出,我们需要一些线程来执行Fork出的任务,在实际中,如果每次都创建新的线程执行任务,对系统资源的开销会很大,所以Fork/Join框架利用了线程池来调度任务。
另外,这里可以思考一个问题,既然由线程池调度,根据我们之前学习普通/计划线程池的经验,必然存在两个要素:
- 工作线程
- 任务队列
一般的线程池只有一个任务队列,但是对于Fork/Join框架来说,由于Fork出的各个子任务其实是平行关系,为了提高效率,减少线程竞争,应该将这些平行的任务放到不同的队列中去,如上图中,大任务分解成三个子任务:子任务1、子任务2、子任务3,那么就创建三个任务队列,然后再创建3个工作线程与队列一一对应。
由于线程处理不同任务的速度不同,这样就可能存在某个线程先执行完了自己队列中的任务的情况,这时为了提升效率,我们可以让该线程去“窃取”其它任务队列中的任务,这就是所谓的工作窃取算法。
“工作窃取”的示意图如下,当线程1执行完自身任务队列中的任务后,尝试从线程2的任务队列中“窃取”任务:
对于一般的队列来说,入队元素都是在“队尾”,出队元素在“队首”,要满足“工作窃取”的需求,任务队列应该支持从“队尾”出队元素,这样可以减少与其它工作线程的冲突(因为正常情况下,其它工作线程从“队首”获取自己任务队列中的任务),满足这一需求的任务队列其实就是双端阻塞队列——LinkedBlockingDeque。
当然,出于性能考虑,J.U.C中的Fork/Join框架并没有直接利用LinkedBlockingDeque作为任务队列,而是自己重新实现了一个。
8.3 Fork/Join组件
该框架主要涉及三大核心组件:ForkJoinPool
(线程池)、ForkJoinTask
(任务)、ForkJoinWorkerThread
(工作线程),外加WorkQueue
(任务队列):
- ForkJoinPool:ExecutorService的实现类,负责工作线程的管理、任务队列的维护,以及控制整个任务调度流程;
- ForkJoinTask:Future接口的实现类,fork是其核心方法,用于分解任务并异步执行;而join方法在任务结果计算完毕之后才会运行,用来合并或返回计算结果;
- ForkJoinWorkerThread:Thread的子类,作为线程池中的工作线程(Worker)执行任务;
- WorkQueue:任务队列,用于保存任务;
8.3.1 ForkJoinPool
它作为Executors框架的一员,是ExecutorService的一个实现类
ForkJoinPool的主要工作如下:
- 接受外部任务的提交(外部调用ForkJoinPool的
invoke
/execute
/submit
方法提交任务); - 接受ForkJoinTask自身fork出的子任务的提交;
- 任务队列数组(
WorkQueue[]
)的初始化和管理; - 工作线程(Worker)的创建/管理。
ForkJoinPool提供了3类外部提交任务的方法:invoke、execute、submit,它们的主要区别在于任务的执行方式上。
- 通过invoke方法提交的任务,调用线程直到任务执行完成才会返回,也就是说这是一个同步方法,且有返回结果;
- 通过execute方法提交的任务,调用线程会立即返回,也就是说这是一个异步方法,且没有返回结果;
- 通过submit方法提交的任务,调用线程会立即返回,也就是说这是一个异步方法,且有返回结果(返回Future实现类,可以通过get获取结果)。
8.3.2 ForkJoinTask
从Fork/Join框架的描述上来看,“任务”必须要满足一定的条件:
- 支持Fork,即任务自身的分解
- 支持Join,即任务结果的合并
ForkJoinTask就是符合这种条件的任务。
ForkJoinTask实现了Future接口,是一个异步任务,我们在使用Fork/Join框架时,一般需要使用线程池来调度任务,线程池内部调度的其实都是ForkJoinTask任务
除了ForkJoinTask,Fork/Join框架还提供了两个ForkJoinTask的抽象实现,我们在自定义ForkJoin任务时,一般继承这两个类:
- RecursiveAction:表示具有返回结果的ForkJoin任务
- RecursiveTask:表示没有返回结果的ForkJoin任务
其它组件就不说了
8.3.3 使用示例
假设有个非常大的long[]数组,通过FJ框架求解数组所有元素的和。
任务类定义,因为需要返回结果,所以继承RecursiveTask,并覆写compute方法。任务的fork通过ForkJoinTask的fork方法执行,join方法方法用于等待任务执行后返回.
代码大致的意思就是:
ArraySumTask类初始化时会传入需要计算的数组,和begin,end。通过设置的THRESHOLD 阈值来与begin,end比较.
如果end - begin + 1 < THRESHOLD,那么不需要分段,
如果end - begin + 1 >THRESHOLD, 就需要分段计算了,怎么分呢?就再次创建两个ArraySumTask 任务,一个处理array的index为0-500的数据,一个处理501-1000的数据。然后再次调用fork方法,会执行新任务的compute方法,那么由于刚创建的两个任务还是比阈值100大,所以分别又会创建任务,就一直递归创建任务,直到end-begin小于阈值。然后分别执行任务,跳出递归,执行join方法,将结果统一相加
public class ArraySumTask extends RecursiveTask<Long> {
private final int[] array;
private final int begin;
private final int end;
private static final int THRESHOLD = 100;
public ArraySumTask(int[] array, int begin, int end) {
this.array = array;
this.begin = begin;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
if (end - begin + 1 < THRESHOLD) { // 小于阈值, 直接计算
for (int i = begin; i <= end; i++) {
sum += array[i];
}
} else {
int middle = (end + begin) / 2;
ArraySumTask subtask1 = new ArraySumTask(this.array, begin, middle);
ArraySumTask subtask2 = new ArraySumTask(this.array, middle + 1, end);
subtask1.fork();
subtask2.fork();
long sum1 = subtask1.join();
long sum2 = subtask2.join();
sum = sum1 + sum2;
}
return sum;
}
}
调用方如下:
public class Main {
public static void main(String[] args) {
ForkJoinPool executor = new ForkJoinPool();
ArraySumTask task = new ArraySumTask(new int[10000], 0, 9999);
ForkJoinTask future = executor.submit(task);
// some time passed...
if (future.isCompletedAbnormally()) {
System.out.println(future.getException());
}
try {
System.out.println("result: " + future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
注意:ForkJoinTask在执行的时候可能会抛出异常,但是没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()
方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException
方法获取异常.