Java多线程(3):取消正在运行的任务

更新于 2018-03-12  约 15 分钟

当一个任务正在运行的过程中,而我们却发现这个任务已经没有必要继续运行了,那么我们便产生了取消任务的需要。比如 上一篇文章 提到的线程池的 invokeAny 方法,它可以在线程池中运行一组任务,当其中任何一个任务完成时,invokeAny 方法便会停止阻塞并返回,同时也会 取消其他任务。那我们如何取消一个正在运行的任务?


前面两篇多线程的文章都有提到 Future<V> 接口和它的一个实现类 FutureTask<V>,并且我们已经知道 Future<V> 可以用来和已经提交的任务进行交互。Future<V> 接口定义了如下几个方法:

Future.java 的源码

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


然后我们来实践下 Futurecancel 方法的功能:

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<Double> future = threadPool.submit(task);
        threadPool.shutdown(); // 发送关闭线程池的指令

        double time = future.get();
        System.out.format("任务运行时间: %.3f s\n", time);

    }

    private static final class SimpleTask implements Callable<Double> {

        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<Double> 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("任务执行出错");
    }

}

运行结果:
取消任务时的运行结果

可以看到,当任务被取消时,Futureget 方法抛出了 CancellationException 异常,并且成功的取消了任务(从构建(运行)总时间可以发现)。


这样就可以了吗?调用 Futurecancel(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<Boolean> 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<Boolean> {

        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 秒的时候调用 Futurecancel(true)

public static void main(String[] args) throws Exception {
    ExecutorService threadPool = Executors.newSingleThreadExecutor();

    long num = 1000000033L;
    PrimerTask task = new PrimerTask(num);
    Future<Boolean> 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 秒时候的输出:
程序运行到 2 秒时候的输出

程序的最终输出:
程序的最终输出

可以发现,虽然我们取消了任务,Futureget 方法也对我们的取消做出了响应(即抛出 CancellationException 异常),但是任务并没有停止,而是直到任务运行完毕了,程序才结束。

查看 Future 的实现类 FutureTask 的源码,我们来看一下调用 cancel(true) 究竟发生了什么:
cancel 的源码

原来 cancel(true) 方法的原理是向正在运行任务的线程发送中断指令 —— 即调用运行任务的 Threadinterrupt() 方法。

所以 如果一个任务是可取消的,那么它应该可以对 Threadinterrupt() 方法做出被取消时的响应

ThreadisInterrupted() 方法,便可以用来判断当前 Thread 是否被中断。任务开始运行时,运行任务的线程肯定没有被中断,所以 isInterruped() 方法会返回 false;而 interrupt() 方法调用之后,isInterruped() 方法会返回 true
(由此我们也可以知道,Thread.sleep 方法是可以对中断做出响应的)

所以我们修改 PrimerTaskcall 方法,让其可以对运行任务的线程被中断时做出停止运行(跳出循环)的响应:

@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 秒的时候停止了运行,任务被成功取消。


总结:如果要通过 Futurecancel 方法取消正在运行的任务,那么该任务必定是可以 对线程中断做出响应 的任务。通过 Thread.currentThread().isInterrupted() 方法,我们可以判断任务是否被取消,从而做出相应的取消任务的响应。

阅读 13.6k更新于 2018-03-12

推荐阅读
Java Follower
用户专栏

Java 平台是博大精深的。作为一名 Java 的追随者,聊聊各种有趣或者有坑的 Java 技术。

65 人关注
23 篇文章
专栏主页
目录