资讯专栏INFORMATION COLUMN

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

terro / 1877人阅读

摘要:比如上一篇文章提到的线程池的方法,它可以在线程池中运行一组任务,当其中任何一个任务完成时,方法便会停止阻塞并返回,同时也会取消其他任务。

当一个任务正在运行的过程中,而我们却发现这个任务已经没有必要继续运行了,那么我们便产生了取消任务的需要。比如 上一篇文章 提到的线程池的 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

然后我们来实践下 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 future = threadPool.submit(task);
        threadPool.shutdown(); // 发送关闭线程池的指令

        double time = future.get();
        System.out.format("任务运行时间: %.3f s
", 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
", 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 future = threadPool.submit(task);
        threadPool.shutdown();
        
        boolean result = future.get();
        System.out.format("%d 是否为素数? %b
", 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 秒的时候调用 Futurecancel(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
", num, result);
    } catch (CancellationException ex) {
        System.err.println("任务被取消");
    } catch (InterruptedException ex) {
        System.err.println("当前线程被中断");
    } catch (ExecutionException ex) {
        System.err.println("任务执行出错");
    }
}

程序运行到 2 秒时候的输出:

程序的最终输出:

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

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

原来 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() 方法,我们可以判断任务是否被取消,从而做出相应的取消任务的响应。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66430.html

相关文章

  • (十八)java线程之Callable Future

    摘要:本人邮箱欢迎转载转载请注明网址代码已经全部托管有需要的同学自行下载引言前面我们讲了那么多有关线程的知识不知道读者有没有想过这么一个问题如果有这么一个比较耗时的任务必须使用线程来执行但是在这个任务执行完之后我需要得到这个线程的返回值以目前我们 本人邮箱: 欢迎转载,转载请注明网址 http://blog.csdn.net/tianshi_kcogithub: https://github...

    stormgens 评论0 收藏0
  • Java线程进阶(四二)—— J.U.C之executors框架:Future模式

    摘要:本文首发于一世流云的专栏一模式简介模式是多线程设计模式中的一种常见模式,它的主要作用就是异步地执行任务,并在需要的时候获取结果。二中的模式在多线程基础之模式中,我们曾经给出过模式的通用类关系图。 showImg(https://segmentfault.com/img/bVbiwcx?w=1000&h=667); 本文首发于一世流云的专栏:https://segmentfault.co...

    marek 评论0 收藏0
  • Java线程(2):使用线程池 ThreadPoolExecutor

    摘要:本文只介绍中线程池的基本使用,不会过多的涉及到线程池的原理。可缓存线程的线程池创建一个可缓存线程的线程池。首先是从接口继承到的方法使用该方法即将一个任务交给线程池去执行。方法方法的作用是向线程池发送关闭的指令。 首先,我们为什么需要线程池?让我们先来了解下什么是 对象池 技术。某些对象(比如线程,数据库连接等),它们创建的代价是非常大的 —— 相比于一般对象,它们创建消耗的时间和内存都...

    darry 评论0 收藏0
  • Java线程-Callable和Future

    摘要:类提供了一些有用的方法在线程池中执行内的任务。在线程池提交任务后返回了一个对象,使用它可以知道任务的状态和得到返回的执行结果。 Callable和Future出现的原因 创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。 这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。 如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达...

    seasonley 评论0 收藏0
  • FutureTask源码解析(2)——深入理解FutureTask

    摘要:本文的源码基于。人如其名,包含了和两部分。而将一个任务的状态设置成终止态只有三种方法我们将在下文的源码解析中分析这三个方法。将栈中所有挂起的线程都唤醒后,下面就是执行方法这个方法是一个空方 前言 系列文章目录 有了上一篇对预备知识的了解之后,分析源码就容易多了,本篇我们就直接来看看FutureTask的源码。 本文的源码基于JDK1.8。 Future和Task 在深入分析源码之前,我...

    Harpsichord1207 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<