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 就是为了解决“任务完成后继续做下一步、多个任务组合、异常兜底”这些问题。