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

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


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

Future.java 的源码

get 方法:经过前面文章的介绍,咱们已经了解了 get 方法的使用 —— get 方法 用来返回和 Future 关联的任务的结果。带参数的 get 方法指定一个超时时间,在超时时间内该方法会阻塞当前线程,直到得到结果 。多线程

  • 若是在给定的超时时间内没有得到结果,那么便抛出 TimeoutException 异常;
  • 或者执行的任务被取消(此时抛出 CancellationException 异常);
  • 或者执行任务时出错,即执行过程当中出现异常(此时抛出 ExecutionException 异常);
  • 或者当前线程被中断(此时抛出 InterruptedException 异常 —— 注意,当前线程是指调用 get 方法的线程,而不是运行任务的线程)。

不带参数的 get 能够理解为超时时间无限大,即一直等待直到得到结果或者出现异常。ide


cancel(boolean mayInterruptIfRunning) 方法:该方法是非阻塞的。经过 JDK 的文档,咱们能够知道 该方法即可以用来(尝试)终止一个任务测试

  • 若是任务运行以前调用了该方法,那么任务就不会被运行;
  • 若是任务已经完成或者已经被取消,那么该方法方法不起做用;
  • 若是任务正在运行,而且 cancel 传入参数为 true,那么便会去终止与 Future 关联的任务。

cancel(false)cancel(true)的区别在于,cancel(false)取消已经提交但尚未被运行的任务(即任务就不会被安排运行);而 cancel(true) 会取消全部已经提交的任务,包括 正在等待的正在运行的 任务。this


isCancelled 方法:该方法是非阻塞的。在任务结束以前,若是任务被取消了,该方法返回 true,不然返回 false;若是任务已经完成,该方法则一直返回 falsespa

isDone 方法:该方法一样是非阻塞的。若是任务已经结束(正常结束,或者被取消,或者执行出错),返回 true,不然返回 false线程


而后咱们来实践下 Futurecancel 方法的功能:3d

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; // 返回任务运行的时间,以 秒 计
        }

    }
}

运行结果(任务正常运行):
任务正常运行的结果code

而后咱们定义一个用来取消任务的方法:

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() 方法,咱们能够判断任务是否被取消,从而作出相应的取消任务的响应。

相关文章
相关标签/搜索