当一个任务正在运行的过程中,而我们却发现这个任务已经没有必要继续运行了,那么我们便产生了取消任务的需要。比如 上一篇文章 提到的线程池的 invokeAny
方法,它可以在线程池中运行一组任务,当其中任何一个任务完成时,invokeAny
方法便会停止阻塞并返回,同时也会 取消其他任务。那我们如何取消一个正在运行的任务?
前面两篇多线程的文章都有提到 Future
接口和它的一个实现类 FutureTask
,并且我们已经知道 Future
可以用来和已经提交的任务进行交互。Future
接口定义了如下几个方法:
get
方法:通过前面文章的介绍,我们已经了解了 get
方法的使用 —— get
方法 用来返回和 Future
关联的任务的结果。带参数的 get
方法指定一个超时时间,在超时时间内该方法会阻塞当前线程,直到获得结果 。
- 如果在给定的超时时间内没有获得结果,那么便抛出
TimeoutException
异常; - 或者执行的任务被取消(此时抛出
CancellationException
异常); - 或者执行任务时出错,即执行过程中出现异常(此时抛出
ExecutionException
异常); - 或者当前线程被中断(此时抛出
InterruptedException
异常 —— 注意,当前线程是指调用get
方法的线程,而不是运行任务的线程)。
不带参数的 get
可以理解为超时时间无限大,即一直等待直到获得结果或者出现异常。
cancel(boolean mayInterruptIfRunning)
方法:该方法是非阻塞的。通过 JDK 的文档,我们可以知道 该方法便可以用来(尝试)终止一个任务。
- 如果任务运行之前调用了该方法,那么任务就不会被运行;
- 如果任务已经完成或者已经被取消,那么该方法方法不起作用;
- 如果任务正在运行,并且
cancel
传入参数为true
,那么便会去终止与Future
关联的任务。
cancel(false)
与 cancel(true)
的区别在于,cancel(false)
只 取消已经提交但还没有被运行的任务(即任务就不会被安排运行);而 cancel(true)
会取消所有已经提交的任务,包括 正在等待的 和 正在运行的 任务。
isCancelled
方法:该方法是非阻塞的。在任务结束之前,如果任务被取消了,该方法返回 true
,否则返回 false
;如果任务已经完成,该方法则一直返回 false
。
isDone
方法:该方法同样是非阻塞的。如果任务已经结束(正常结束,或者被取消,或者执行出错),返回 true
,否则返回 false
。
然后我们来实践下 Future
的 cancel
方法的功能:
import java.util.concurrent.*;
public class FutureTest {
public static void main(String[] args) throws Exception {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
SimpleTask task = new SimpleTask(3_000); // task 需要运行 3 秒
Future future = threadPool.submit(task);
threadPool.shutdown(); // 发送关闭线程池的指令
double time = future.get();
System.out.format("任务运行时间: %.3f s\n", time);
}
private static final class SimpleTask implements Callable {
private final int sleepTime; // ms
public SimpleTask(int sleepTime) {
this.sleepTime = sleepTime;
}
@Override
public Double call() throws Exception {
double begin = System.nanoTime();
Thread.sleep(sleepTime);
double end = System.nanoTime();
double time = (end - begin) / 1E9;
return time; // 返回任务运行的时间,以 秒 计
}
}
}
运行结果(任务正常运行):
然后我们定义一个用来取消任务的方法:
private static void cancelTask(final Future> future, final int delay) {
Runnable cancellation = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(delay);
future.cancel(true); // 取消与 future 关联的正在运行的任务
} catch (InterruptedException ex) {
ex.printStackTrace(System.err);
}
}
};
new Thread(cancellation).start();
}
然后修改 main
方法:
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
SimpleTask task = new SimpleTask(3_000); // task 需要运行 3 秒
Future future = threadPool.submit(task);
threadPool.shutdown(); // 发送关闭线程池的指令
cancelTask(future, 2_000); // 在 2 秒之后取消该任务
try {
double time = future.get();
System.out.format("任务运行时间: %.3f s\n", time);
} catch (CancellationException ex) {
System.err.println("任务被取消");
} catch (InterruptedException ex) {
System.err.println("当前线程被中断");
} catch (ExecutionException ex) {
System.err.println("任务执行出错");
}
}
运行结果:
可以看到,当任务被取消时,Future
的 get
方法抛出了 CancellationException
异常,并且成功的取消了任务(从构建(运行)总时间可以发现)。
这样就可以了吗?调用 Future
的 cancel(true)
就一定能取消正在运行的任务吗?
我们来写一个真正的耗时任务,判断一个数是否为素数,测试数据为 1000000033 (它是一个素数)。
import java.util.concurrent.*;
public class FutureTest {
public static void main(String[] args) throws Exception {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
long num = 1000000033L;
PrimerTask task = new PrimerTask(num);
Future future = threadPool.submit(task);
threadPool.shutdown();
boolean result = future.get();
System.out.format("%d 是否为素数? %b\n", num, result);
}
private static final class PrimerTask implements Callable {
private final long num;
public PrimerTask(long num) {
this.num = num;
}
@Override
public Boolean call() throws Exception {
// i < num 让任务有足够的运行时间
for (long i = 2; i < num; i++) {
if (num % i == 0) {
return false;
}
}
return true;
}
}
}
在我的机器上,这个任务需要 13 秒才能运行完毕:
然后我们修改 main
方法,在任务运行到 2 秒的时候调用 Future
的 cancel(true)
:
public static void main(String[] args) throws Exception {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
long num = 1000000033L;
PrimerTask task = new PrimerTask(num);
Future future = threadPool.submit(task);
threadPool.shutdown(); // 发送关闭线程池的指令
cancelTask(future, 2_000); // 在 2 秒之后取消该任务
try {
boolean result = future.get();
System.out.format("%d 是否为素数? %b\n", num, result);
} catch (CancellationException ex) {
System.err.println("任务被取消");
} catch (InterruptedException ex) {
System.err.println("当前线程被中断");
} catch (ExecutionException ex) {
System.err.println("任务执行出错");
}
}
程序运行到 2 秒时候的输出:
程序的最终输出:
可以发现,虽然我们取消了任务,Future
的 get
方法也对我们的取消做出了响应(即抛出 CancellationException
异常),但是任务并没有停止,而是直到任务运行完毕了,程序才结束。
查看 Future
的实现类 FutureTask
的源码,我们来看一下调用 cancel(true)
究竟发生了什么:
原来 cancel(true)
方法的原理是向正在运行任务的线程发送中断指令 —— 即调用运行任务的 Thread
的 interrupt()
方法。
所以 如果一个任务是可取消的,那么它应该可以对 Thread
的 interrupt()
方法做出被取消时的响应。
而 Thread
的 isInterrupted()
方法,便可以用来判断当前 Thread
是否被中断。任务开始运行时,运行任务的线程肯定没有被中断,所以 isInterruped()
方法会返回 false
;而 interrupt()
方法调用之后,isInterruped()
方法会返回 true
。
(由此我们也可以知道,Thread.sleep
方法是可以对中断做出响应的)
所以我们修改 PrimerTask
的 call
方法,让其可以对运行任务的线程被中断时做出停止运行(跳出循环)的响应:
@Override
public Boolean call() throws Exception {
// i < num 让任务有足够的运行时间
for (long i = 2; i < num; i++) {
if (Thread.currentThread().isInterrupted()) { // 任务被取消
System.out.println("PrimerTask.call: 你取消我干啥?");
return false;
}
if (num % i == 0) {
return false;
}
}
return true;
}
运行结果:
可以看到程序在 2 秒的时候停止了运行,任务被成功取消。
总结:如果要通过 Future
的 cancel
方法取消正在运行的任务,那么该任务必定是可以 对线程中断做出响应 的任务。通过 Thread.currentThread().isInterrupted()
方法,我们可以判断任务是否被取消,从而做出相应的取消任务的响应。