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() 方法,咱們能夠判斷任務是否被取消,從而作出相應的取消任務的響應。

相關文章
相關標籤/搜索