跳到正文
hello world

26. Future 超时、取消和任务状态判断

发布于阅读量 0

26. Future 超时、取消和任务状态判断

上一节我用 Future 拿到了异步任务的结果。

最核心的方法是:

future.get();

它可以拿到线程池任务的返回值。

但是 get() 有一个问题:它会一直等

如果某个 PDF 文件特别大,或者 PDFBox 处理时卡住了,future.get() 就可能一直阻塞在那里。对于批量任务来说,这肯定不太合适。

所以这一节继续看 Future 里的几个常用方法:

get(timeout):最多等多久;
cancel(true):尝试取消任务;
isDone():任务是否结束;
isCancelled():任务是否被取消。

get() 默认会一直等

先看最普通的 get()

String result = future.get();

它的意思是:

任务没完成,我就一直等;
任务完成了,我再拿结果。

这在任务耗时可控时没什么问题。

但如果任务卡住,比如:

Thread.sleep(100000);

或者真实场景里某个 PDF 处理一直不返回,那当前线程就会一直卡在 get()

如果这是 main 线程,程序就停在那里。

如果这是 Web 请求线程,接口就会一直不响应。

所以实际项目里,很多时候不能无脑用无限等待的 get()


使用 get(timeout) 设置等待时间

Future 提供了带超时时间的 get()

future.get(3, TimeUnit.SECONDS);

意思是:

最多等 3 秒;
3 秒内任务完成,就返回结果;
3 秒内任务没完成,就抛 TimeoutException。

这比一直等更安全。

新建类:

com.succos.threadpool.FutureTimeoutDemo

代码如下:

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class FutureTimeoutDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        Future<String> future = executor.submit(() -> {
            System.out.println(Thread.currentThread().getName()
                    + " 开始处理 PDF");

            Thread.sleep(5000);

            System.out.println(Thread.currentThread().getName()
                    + " PDF 处理完成");

            return "PDF 处理成功";
        });

        try {
            System.out.println("main 最多等待 3 秒");

            String result = future.get(3, TimeUnit.SECONDS);

            System.out.println("main 拿到结果:" + result);

        } catch (TimeoutException e) {
            System.out.println("等待超时,PDF 还没有处理完");

        } catch (Exception e) {
            e.printStackTrace();
        }

        executor.shutdown();
    }
}

这个任务里睡了 5 秒:

Thread.sleep(5000);

但主线程只等 3 秒:

future.get(3, TimeUnit.SECONDS);

所以会进入:

catch (TimeoutException e)

输出:

等待超时,PDF 还没有处理完

超时不等于任务已经停止

这里要注意一个很重要的点。

future.get(3, TimeUnit.SECONDS) 超时,只代表:

当前线程最多等了 3 秒,没有等到结果。

它不代表任务线程已经停止。

也就是说,后台任务可能还在继续执行。

比如上面的例子里,主线程 3 秒后打印了“等待超时”,但是线程池里的任务可能还会继续睡到 5 秒,然后继续打印:

pool-1-thread-1 PDF 处理完成

所以:

get(timeout) 控制的是等待时间;
不是强制终止任务。

这个点很容易误解。

如果我希望超时后尝试停止任务,就要配合 cancel(true)


cancel(true):尝试取消任务

Future 可以调用:

future.cancel(true);

它的意思是:

尝试取消这个任务;
如果任务还没开始,尽量不让它执行;
如果任务已经在执行,尝试中断执行它的线程。

注意这里是“尝试”。

它不是强制杀死线程。

Java 里没有安全的“强杀线程”方式。

cancel(true) 更准确地说,是给执行任务的线程发送一个中断信号。

任务能不能停下来,要看任务代码有没有响应中断。


超时后取消任务

把刚才的代码改一下。

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class FutureCancelDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        Future<String> future = executor.submit(() -> {
            System.out.println(Thread.currentThread().getName()
                    + " 开始处理 PDF");

            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName()
                        + " 收到中断信号,任务准备结束");

                Thread.currentThread().interrupt();

                return "PDF 处理被取消";
            }

            System.out.println(Thread.currentThread().getName()
                    + " PDF 处理完成");

            return "PDF 处理成功";
        });

        try {
            System.out.println("main 最多等待 3 秒");

            String result = future.get(3, TimeUnit.SECONDS);

            System.out.println("main 拿到结果:" + result);

        } catch (TimeoutException e) {
            System.out.println("等待超时,准备取消任务");

            boolean cancelled = future.cancel(true);

            System.out.println("取消结果:" + cancelled);

        } catch (Exception e) {
            e.printStackTrace();
        }

        executor.shutdown();
    }
}

这里任务本来要睡 10 秒。

主线程最多等 3 秒。

超时后调用:

future.cancel(true);

因为任务正好在 Thread.sleep() 中,sleep() 能响应中断,所以它会抛出 InterruptedException

然后任务里会打印:

收到中断信号,任务准备结束

cancel(true) 不是强制停止

虽然上面的例子里,任务被中断了,但这是因为 Thread.sleep() 会响应中断。

如果任务代码是这样:

while (true) {
    // 一直计算,不检查中断状态
}

cancel(true) 可能并不能让它马上停止。

因为线程收到了中断信号,但任务代码不理它。

所以我现在会这样理解:

cancel(true) 只是发出中断请求;
任务是否停止,要看任务代码是否配合。

比较好的任务代码,应该在合适的位置检查中断状态。

比如:

if (Thread.currentThread().isInterrupted()) {
    return "任务被中断";
}

或者在捕获 InterruptedException 后及时退出。


为什么捕获 InterruptedException 后要重新设置中断状态

前面代码里有一行:

Thread.currentThread().interrupt();

它的作用是恢复当前线程的中断状态。

因为当 Thread.sleep() 抛出 InterruptedException 时,线程的中断标记会被清除。

如果我只是捕获异常,然后什么都不做,外层代码可能就不知道这个线程曾经被中断过。

所以比较常见的写法是:

catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}

在有返回值的任务里,可以返回一个失败结果。

比如:

catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return PdfTaskResult.fail(fileName, "任务被中断");
}

这个习惯在并发代码里很重要。


isDone:任务是否结束

Future 还有一个方法:

future.isDone();

它表示任务是否已经结束。

这里的“结束”包含几种情况:

正常完成;
异常结束;
被取消。

只要任务不再继续执行了,isDone() 通常就是 true

可以写一个简单例子。

新建类:

com.succos.threadpool.FutureStatusDemo

代码如下:

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class FutureStatusDemo {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        Future<String> future = executor.submit(() -> {
            System.out.println("任务开始执行");

            Thread.sleep(3000);

            System.out.println("任务执行完成");

            return "处理成功";
        });

        System.out.println("刚提交后 isDone:" + future.isDone());

        Thread.sleep(1000);

        System.out.println("1 秒后 isDone:" + future.isDone());

        String result = future.get();

        System.out.println("结果:" + result);

        System.out.println("get 后 isDone:" + future.isDone());

        executor.shutdown();
    }
}

输出大概是:

刚提交后 isDone:false
1 秒后 isDone:false
任务执行完成
结果:处理成功
get 后 isDone:true

isDone() 适合判断任务是否已经结束。

不过它只是状态判断,不会拿结果。

真正拿结果还是要 get()


isCancelled:任务是否被取消

另一个状态方法是:

future.isCancelled();

它表示任务是否被取消。

新建类:

com.succos.threadpool.FutureCancelStatusDemo

代码如下:

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class FutureCancelStatusDemo {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,
                1,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        Future<String> future = executor.submit(() -> {
            System.out.println("任务开始执行");

            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println("任务收到中断信号");
                Thread.currentThread().interrupt();
            }

            return "任务结束";
        });

        Thread.sleep(1000);

        System.out.println("取消前 isDone:" + future.isDone());
        System.out.println("取消前 isCancelled:" + future.isCancelled());

        boolean cancelled = future.cancel(true);

        System.out.println("cancel 返回:" + cancelled);

        Thread.sleep(1000);

        System.out.println("取消后 isDone:" + future.isDone());
        System.out.println("取消后 isCancelled:" + future.isCancelled());

        executor.shutdown();
    }
}

可能输出:

任务开始执行
取消前 isDone:false
取消前 isCancelled:false
任务收到中断信号
cancel 返回:true
取消后 isDone:true
取消后 isCancelled:true

被取消以后,isDone() 通常也是 true

因为取消也代表任务已经结束了。

所以:

isDone:任务是否结束;
isCancelled:任务是否因为取消而结束。

cancel(false) 和 cancel(true) 的区别

cancel() 里有一个参数:

future.cancel(true);

这个参数叫 mayInterruptIfRunning

如果传 true

如果任务已经在执行,尝试中断执行它的线程。

如果传 false

如果任务已经在执行,就不去中断它;
如果任务还没开始,可能会取消它。

所以:

future.cancel(false);

更温和。

但如果任务已经跑起来了,它一般不会被打断。

而:

future.cancel(true);

会尝试中断正在执行的任务。

PDF 处理这种任务要慎重。

如果已经在写文件时被中断,可能会留下半成品文件。

所以真实项目里,取消任务时最好考虑:

是否使用临时文件;
失败后是否删除半成品;
数据库状态是否更新为失败;
日志是否记录清楚;
用户是否能重新提交。

PDF 处理超时时怎么设计

如果某个 PDF 处理超过 30 秒,我可能会这样处理:

try {
    PdfTaskResult result = future.get(30, TimeUnit.SECONDS);
    return result;

} catch (TimeoutException e) {
    future.cancel(true);
    return PdfTaskResult.fail(fileName, "PDF 处理超时");
}

但这里要记住:

cancel(true) 不一定能立刻停止 PDFBox 正在做的事情。

所以更稳的设计是:

输出文件先写到临时路径;
处理成功后再重命名成正式文件;
如果失败或超时,清理临时文件;
任务状态写入数据库;
不要让用户下载半成品。

比如:

output/a-watermark.tmp.pdf
处理成功后改名为:
output/a-watermark.pdf

这样即使中途失败,也不容易把半成品当成正式结果。


批量 Future 里怎么加超时

如果是批量 PDF,每个任务一个 Future,我可以在统一获取结果时给每个任务设置超时:

for (Future<PdfTaskResult> future : futureList) {
    try {
        PdfTaskResult result = future.get(30, TimeUnit.SECONDS);

        // 处理结果

    } catch (TimeoutException e) {
        future.cancel(true);

        System.out.println("某个 PDF 处理超时");

    } catch (Exception e) {
        System.out.println("获取任务结果失败:" + e.getMessage());
    }
}

不过这个写法也有一个细节。

如果我按顺序遍历 futureList,每个 get(30秒) 都可能等一段时间。

假设第一个任务卡住了,主线程会先在第一个任务那里等 30 秒,后面的结果要等它处理完超时后才会继续取。

所以 Future 虽然能做超时,但批量任务编排不是特别舒服。

这也是后面要学 CompletableFuture 的原因之一。


Future 的状态判断怎么用

isDone()isCancelled() 可以用来做状态检查,但我不会把它们当成主要的结果处理方式。

比如:

if (future.isDone()) {
    String result = future.get();
}

这种写法可以,但实际批量处理中,我更多还是直接 get(),然后处理成功、失败、超时。

状态方法更适合:

调试;
轮询任务状态;
取消前判断;
日志观察。

比如:

if (!future.isDone()) {
    System.out.println("任务还没结束");
}

但最终要拿结果,还是绕不开 get()


这一节小结

这一节我主要记住几点:

1. future.get() 会一直等待任务完成;
2. future.get(timeout, unit) 可以设置最多等待多久;
3. 超时只代表当前线程不等了,不代表任务已经停止;
4. future.cancel(true) 是尝试中断任务,不是强制杀死线程;
5. 任务能不能被取消,要看任务代码是否响应中断;
6. isDone() 表示任务是否结束;
7. isCancelled() 表示任务是否被取消;
8. PDF 处理超时后,要考虑半成品文件清理和任务状态更新。

用一句话总结:

Future 能拿异步结果,但 get 会阻塞,所以真实任务里要考虑超时、取消和失败清理。

下一节开始进入 CompletableFuture

Future 能拿结果,但异步流程编排不够顺手。CompletableFuture 就是为了解决“任务完成后继续做下一步、多个任务组合、异常兜底”这些问题。## 26. Future 超时、取消和任务状态判断

上一节我用 Future 拿到了异步任务的结果。

最核心的方法是:

future.get();

它可以拿到线程池任务的返回值。

但是 get() 有一个问题:它会一直等

如果某个 PDF 文件特别大,或者 PDFBox 处理时卡住了,future.get() 就可能一直阻塞在那里。对于批量任务来说,这肯定不太合适。

所以这一节继续看 Future 里的几个常用方法:

get(timeout):最多等多久;
cancel(true):尝试取消任务;
isDone():任务是否结束;
isCancelled():任务是否被取消。

get() 默认会一直等

先看最普通的 get()

String result = future.get();

它的意思是:

任务没完成,我就一直等;
任务完成了,我再拿结果。

这在任务耗时可控时没什么问题。

但如果任务卡住,比如:

Thread.sleep(100000);

或者真实场景里某个 PDF 处理一直不返回,那当前线程就会一直卡在 get()

如果这是 main 线程,程序就停在那里。

如果这是 Web 请求线程,接口就会一直不响应。

所以实际项目里,很多时候不能无脑用无限等待的 get()


使用 get(timeout) 设置等待时间

Future 提供了带超时时间的 get()

future.get(3, TimeUnit.SECONDS);

意思是:

最多等 3 秒;
3 秒内任务完成,就返回结果;
3 秒内任务没完成,就抛 TimeoutException。

这比一直等更安全。

新建类:

com.succos.threadpool.FutureTimeoutDemo

代码如下:

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class FutureTimeoutDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        Future<String> future = executor.submit(() -> {
            System.out.println(Thread.currentThread().getName()
                    + " 开始处理 PDF");

            Thread.sleep(5000);

            System.out.println(Thread.currentThread().getName()
                    + " PDF 处理完成");

            return "PDF 处理成功";
        });

        try {
            System.out.println("main 最多等待 3 秒");

            String result = future.get(3, TimeUnit.SECONDS);

            System.out.println("main 拿到结果:" + result);

        } catch (TimeoutException e) {
            System.out.println("等待超时,PDF 还没有处理完");

        } catch (Exception e) {
            e.printStackTrace();
        }

        executor.shutdown();
    }
}

这个任务里睡了 5 秒:

Thread.sleep(5000);

但主线程只等 3 秒:

future.get(3, TimeUnit.SECONDS);

所以会进入:

catch (TimeoutException e)

输出:

等待超时,PDF 还没有处理完

超时不等于任务已经停止

这里要注意一个很重要的点。

future.get(3, TimeUnit.SECONDS) 超时,只代表:

当前线程最多等了 3 秒,没有等到结果。

它不代表任务线程已经停止。

也就是说,后台任务可能还在继续执行。

比如上面的例子里,主线程 3 秒后打印了“等待超时”,但是线程池里的任务可能还会继续睡到 5 秒,然后继续打印:

pool-1-thread-1 PDF 处理完成

所以:

get(timeout) 控制的是等待时间;
不是强制终止任务。

这个点很容易误解。

如果我希望超时后尝试停止任务,就要配合 cancel(true)


cancel(true):尝试取消任务

Future 可以调用:

future.cancel(true);

它的意思是:

尝试取消这个任务;
如果任务还没开始,尽量不让它执行;
如果任务已经在执行,尝试中断执行它的线程。

注意这里是“尝试”。

它不是强制杀死线程。

Java 里没有安全的“强杀线程”方式。

cancel(true) 更准确地说,是给执行任务的线程发送一个中断信号。

任务能不能停下来,要看任务代码有没有响应中断。


超时后取消任务

把刚才的代码改一下。

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class FutureCancelDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        Future<String> future = executor.submit(() -> {
            System.out.println(Thread.currentThread().getName()
                    + " 开始处理 PDF");

            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName()
                        + " 收到中断信号,任务准备结束");

                Thread.currentThread().interrupt();

                return "PDF 处理被取消";
            }

            System.out.println(Thread.currentThread().getName()
                    + " PDF 处理完成");

            return "PDF 处理成功";
        });

        try {
            System.out.println("main 最多等待 3 秒");

            String result = future.get(3, TimeUnit.SECONDS);

            System.out.println("main 拿到结果:" + result);

        } catch (TimeoutException e) {
            System.out.println("等待超时,准备取消任务");

            boolean cancelled = future.cancel(true);

            System.out.println("取消结果:" + cancelled);

        } catch (Exception e) {
            e.printStackTrace();
        }

        executor.shutdown();
    }
}

这里任务本来要睡 10 秒。

主线程最多等 3 秒。

超时后调用:

future.cancel(true);

因为任务正好在 Thread.sleep() 中,sleep() 能响应中断,所以它会抛出 InterruptedException

然后任务里会打印:

收到中断信号,任务准备结束

cancel(true) 不是强制停止

虽然上面的例子里,任务被中断了,但这是因为 Thread.sleep() 会响应中断。

如果任务代码是这样:

while (true) {
    // 一直计算,不检查中断状态
}

cancel(true) 可能并不能让它马上停止。

因为线程收到了中断信号,但任务代码不理它。

所以我现在会这样理解:

cancel(true) 只是发出中断请求;
任务是否停止,要看任务代码是否配合。

比较好的任务代码,应该在合适的位置检查中断状态。

比如:

if (Thread.currentThread().isInterrupted()) {
    return "任务被中断";
}

或者在捕获 InterruptedException 后及时退出。


为什么捕获 InterruptedException 后要重新设置中断状态

前面代码里有一行:

Thread.currentThread().interrupt();

它的作用是恢复当前线程的中断状态。

因为当 Thread.sleep() 抛出 InterruptedException 时,线程的中断标记会被清除。

如果我只是捕获异常,然后什么都不做,外层代码可能就不知道这个线程曾经被中断过。

所以比较常见的写法是:

catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}

在有返回值的任务里,可以返回一个失败结果。

比如:

catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return PdfTaskResult.fail(fileName, "任务被中断");
}

这个习惯在并发代码里很重要。


isDone:任务是否结束

Future 还有一个方法:

future.isDone();

它表示任务是否已经结束。

这里的“结束”包含几种情况:

正常完成;
异常结束;
被取消。

只要任务不再继续执行了,isDone() 通常就是 true

可以写一个简单例子。

新建类:

com.succos.threadpool.FutureStatusDemo

代码如下:

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class FutureStatusDemo {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        Future<String> future = executor.submit(() -> {
            System.out.println("任务开始执行");

            Thread.sleep(3000);

            System.out.println("任务执行完成");

            return "处理成功";
        });

        System.out.println("刚提交后 isDone:" + future.isDone());

        Thread.sleep(1000);

        System.out.println("1 秒后 isDone:" + future.isDone());

        String result = future.get();

        System.out.println("结果:" + result);

        System.out.println("get 后 isDone:" + future.isDone());

        executor.shutdown();
    }
}

输出大概是:

刚提交后 isDone:false
1 秒后 isDone:false
任务执行完成
结果:处理成功
get 后 isDone:true

isDone() 适合判断任务是否已经结束。

不过它只是状态判断,不会拿结果。

真正拿结果还是要 get()


isCancelled:任务是否被取消

另一个状态方法是:

future.isCancelled();

它表示任务是否被取消。

新建类:

com.succos.threadpool.FutureCancelStatusDemo

代码如下:

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class FutureCancelStatusDemo {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,
                1,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        Future<String> future = executor.submit(() -> {
            System.out.println("任务开始执行");

            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println("任务收到中断信号");
                Thread.currentThread().interrupt();
            }

            return "任务结束";
        });

        Thread.sleep(1000);

        System.out.println("取消前 isDone:" + future.isDone());
        System.out.println("取消前 isCancelled:" + future.isCancelled());

        boolean cancelled = future.cancel(true);

        System.out.println("cancel 返回:" + cancelled);

        Thread.sleep(1000);

        System.out.println("取消后 isDone:" + future.isDone());
        System.out.println("取消后 isCancelled:" + future.isCancelled());

        executor.shutdown();
    }
}

可能输出:

任务开始执行
取消前 isDone:false
取消前 isCancelled:false
任务收到中断信号
cancel 返回:true
取消后 isDone:true
取消后 isCancelled:true

被取消以后,isDone() 通常也是 true

因为取消也代表任务已经结束了。

所以:

isDone:任务是否结束;
isCancelled:任务是否因为取消而结束。

cancel(false) 和 cancel(true) 的区别

cancel() 里有一个参数:

future.cancel(true);

这个参数叫 mayInterruptIfRunning

如果传 true

如果任务已经在执行,尝试中断执行它的线程。

如果传 false

如果任务已经在执行,就不去中断它;
如果任务还没开始,可能会取消它。

所以:

future.cancel(false);

更温和。

但如果任务已经跑起来了,它一般不会被打断。

而:

future.cancel(true);

会尝试中断正在执行的任务。

PDF 处理这种任务要慎重。

如果已经在写文件时被中断,可能会留下半成品文件。

所以真实项目里,取消任务时最好考虑:

是否使用临时文件;
失败后是否删除半成品;
数据库状态是否更新为失败;
日志是否记录清楚;
用户是否能重新提交。

PDF 处理超时时怎么设计

如果某个 PDF 处理超过 30 秒,我可能会这样处理:

try {
    PdfTaskResult result = future.get(30, TimeUnit.SECONDS);
    return result;

} catch (TimeoutException e) {
    future.cancel(true);
    return PdfTaskResult.fail(fileName, "PDF 处理超时");
}

但这里要记住:

cancel(true) 不一定能立刻停止 PDFBox 正在做的事情。

所以更稳的设计是:

输出文件先写到临时路径;
处理成功后再重命名成正式文件;
如果失败或超时,清理临时文件;
任务状态写入数据库;
不要让用户下载半成品。

比如:

output/a-watermark.tmp.pdf
处理成功后改名为:
output/a-watermark.pdf

这样即使中途失败,也不容易把半成品当成正式结果。


批量 Future 里怎么加超时

如果是批量 PDF,每个任务一个 Future,我可以在统一获取结果时给每个任务设置超时:

for (Future<PdfTaskResult> future : futureList) {
    try {
        PdfTaskResult result = future.get(30, TimeUnit.SECONDS);

        // 处理结果

    } catch (TimeoutException e) {
        future.cancel(true);

        System.out.println("某个 PDF 处理超时");

    } catch (Exception e) {
        System.out.println("获取任务结果失败:" + e.getMessage());
    }
}

不过这个写法也有一个细节。

如果我按顺序遍历 futureList,每个 get(30秒) 都可能等一段时间。

假设第一个任务卡住了,主线程会先在第一个任务那里等 30 秒,后面的结果要等它处理完超时后才会继续取。

所以 Future 虽然能做超时,但批量任务编排不是特别舒服。

这也是后面要学 CompletableFuture 的原因之一。


Future 的状态判断怎么用

isDone()isCancelled() 可以用来做状态检查,但我不会把它们当成主要的结果处理方式。

比如:

if (future.isDone()) {
    String result = future.get();
}

这种写法可以,但实际批量处理中,我更多还是直接 get(),然后处理成功、失败、超时。

状态方法更适合:

调试;
轮询任务状态;
取消前判断;
日志观察。

比如:

if (!future.isDone()) {
    System.out.println("任务还没结束");
}

但最终要拿结果,还是绕不开 get()


这一节小结

这一节我主要记住几点:

1. future.get() 会一直等待任务完成;
2. future.get(timeout, unit) 可以设置最多等待多久;
3. 超时只代表当前线程不等了,不代表任务已经停止;
4. future.cancel(true) 是尝试中断任务,不是强制杀死线程;
5. 任务能不能被取消,要看任务代码是否响应中断;
6. isDone() 表示任务是否结束;
7. isCancelled() 表示任务是否被取消;
8. PDF 处理超时后,要考虑半成品文件清理和任务状态更新。

用一句话总结:

Future 能拿异步结果,但 get 会阻塞,所以真实任务里要考虑超时、取消和失败清理。

下一节开始进入 CompletableFuture

Future 能拿结果,但异步流程编排不够顺手。CompletableFuture 就是为了解决“任务完成后继续做下一步、多个任务组合、异常兜底”这些问题。

26. Future 超时、取消和任务状态判断 - Java并发之从Thread到CompletableFuture - 上下文网