在Android、桌面端等带界面的应用开发中,异步任务处理是常见需求,很多开发者会选择线程池来管理异步任务。当线程池的工作队列满了之后,新提交的任务会触发拒绝策略,而不同的拒绝策略对提交任务的调用者线程影响差异极大,处理不当就会引发界面卡死问题。

线程池拒绝策略与调用者线程的关系
Java线程池提供了四种内置拒绝策略,其中CallerRunsPolicy策略会直接让提交任务的调用者线程来执行被拒绝的任务,这是导致界面卡死的核心诱因。我们先看四种内置策略的行为差异:
| 拒绝策略 | 行为描述 | 对调用者线程的影响 |
|---|---|---|
| AbortPolicy | 直接抛出RejectedExecutionException异常 | 无阻塞,仅抛出异常 |
| DiscardPolicy | 直接丢弃被拒绝的任务 | 无阻塞,无异常 |
| DiscardOldestPolicy | 丢弃队列中最旧的任务,尝试重新提交当前任务 | 无阻塞,无异常 |
| CallerRunsPolicy | 由提交任务的调用者线程执行该任务 | 调用者线程会被阻塞直到任务执行完成 |
背压机制下界面卡死的场景复现
背压机制本质是当下游处理能力不足时,上游降低任务提交速度或者暂停提交。如果我们在主线程提交异步任务到线程池,同时使用了CallerRunsPolicy策略,当线程池满载时,被拒绝的任务就会回到主线程执行,导致主线程无法处理界面绘制、事件响应等逻辑,最终出现界面卡死。
下面我们模拟一个界面点击触发任务提交的场景,使用CallerRunsPolicy策略,快速多次点击按钮触发高并发任务提交:
import java.util.concurrent.*;
public class UiBlockDemo {
// 模拟主线程的线程池,核心线程2,最大线程2,队列容量2
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
2,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(2),
new ThreadPoolExecutor.CallerRunsPolicy() // 使用调用者线程执行策略
);
public static void main(String[] args) {
// 模拟快速提交6个任务,超过线程池处理能力
for (int i = 0; i < 6; i++) {
int taskId = i;
try {
executor.submit(() -> {
System.out.println("任务" + taskId + "开始执行,线程名:" + Thread.currentThread().getName());
try {
// 模拟任务执行耗时1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务" + taskId + "执行完成");
});
System.out.println("任务" + taskId + "提交成功");
} catch (Exception e) {
System.out.println("任务" + taskId + "提交失败:" + e.getMessage());
}
}
executor.shutdown();
}
}
运行上述代码后,你会发现当线程池满载后,后续提交的任务会由主线程(main线程)执行,主线程会被阻塞在任务执行过程中,无法处理其他逻辑,对应到界面场景就是点击无响应、界面卡死。
预防界面卡死的实战方案
1. 避免使用CallerRunsPolicy作为主线程提交任务的线程池策略
如果任务提交方是主线程,绝对不要使用CallerRunsPolicy策略,优先选择AbortPolicy或者自定义拒绝策略,让主线程仅负责提交任务,不执行具体业务逻辑。
2. 自定义拒绝策略实现背压控制
我们可以自定义拒绝策略,当任务被拒绝时,让调用者线程暂时休眠或者将任务缓存到本地队列,后续再重试提交,避免调用者线程被阻塞。下面是一个简单的背压控制拒绝策略示例:
import java.util.concurrent.*;
public class BackPressureRejectPolicy implements RejectedExecutionHandler {
private final long sleepTimeMs;
public BackPressureRejectPolicy(long sleepTimeMs) {
this.sleepTimeMs = sleepTimeMs;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 让调用者线程休眠一段时间,降低提交速度,实现背压
try {
System.out.println("任务被拒绝,调用者线程休眠" + sleepTimeMs + "ms后重试");
Thread.sleep(sleepTimeMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 休眠后重新尝试提交任务
executor.execute(r);
}
}
3. 结合任务优先级与队列容量动态调整
可以根据业务场景设置合理的队列容量,同时给任务设置优先级,当队列快满时优先处理高优先级任务,低优先级任务直接丢弃或者延迟处理,避免高优先级任务被阻塞。同时可以监控线程池的状态,当队列使用率超过阈值时,主动提示用户当前系统繁忙,降低用户提交任务的频率。
方案验证
我们使用自定义背压拒绝策略替换之前的CallerRunsPolicy,再次运行任务提交代码:
import java.util.concurrent.*;
public class UiSafeDemo {
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
2,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(2),
new BackPressureRejectPolicy(500) // 使用自定义背压策略,拒绝后休眠500ms
);
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
int taskId = i;
try {
executor.submit(() -> {
System.out.println("任务" + taskId + "开始执行,线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务" + taskId + "执行完成");
});
System.out.println("任务" + taskId + "提交成功");
} catch (Exception e) {
System.out.println("任务" + taskId + "提交失败:" + e.getMessage());
}
}
executor.shutdown();
}
}
运行后可以看到,被拒绝的任务不会阻塞主线程,主线程只是短暂休眠后重新提交,不会长时间占用主线程资源,对应到界面场景就不会出现卡死问题。
总结
预防背压机制导致的界面卡死,核心是分析拒绝策略对调用者线程的影响,避免让主线程执行被拒绝的异步任务。实际开发中需要根据任务提交方、任务优先级、业务容忍度选择合适的拒绝策略,必要时自定义拒绝策略实现背压控制,从线程调度层面保障主线程的流畅性。