如何避免ScheduledThreadPoolExecutor的scheduleAtFixedRate任务堆积?
解决ScheduledThreadPoolExecutor任务堆积导致的连续执行问题
问题代码
public static void main(String[] args){ ScheduledThreadPoolExecutor stpe = new ScheduledThreadPoolExecutor(5); AtomicInteger i = new AtomicInteger(0); Runnable runnable = () -> { if(i.get() < 5){ try{ Thread.sleep(2_000); }catch(Exception ignored){ } }else if(i.get() >= 15){ System.exit(0); } System.out.println(i.getAndIncrement() + " - " + stpe.getQueue().size() + " - " + LocalDateTime.now()); }; ScheduledFuture<?> sf = stpe.scheduleAtFixedRate(new Thread(runnable, "Testing"), 0, 1_000, TimeUnit.MILLISECONDS); }
执行输出
0 - 0 - 2023-03-27T13:40:04.865501800 1 - 0 - 2023-03-27T13:40:06.886657500 2 - 0 - 2023-03-27T13:40:08.890066 3 - 0 - 2023-03-27T13:40:10.894186200 4 - 0 - 2023-03-27T13:40:12.898171 5 - 0 - 2023-03-27T13:40:12.899077100 <--- 6 - 0 - 2023-03-27T13:40:12.899077100 <--- 7 - 0 - 2023-03-27T13:40:12.900068700 <--- 8 - 0 - 2023-03-27T13:40:12.900068700 <--- 9 - 0 - 2023-03-27T13:40:12.900068700 <--- 10 - 0 - 2023-03-27T13:40:12.901073800 <--- 11 - 0 - 2023-03-27T13:40:13.857069500 12 - 0 - 2023-03-27T13:40:14.852331500 13 - 0 - 2023-03-27T13:40:15.860971200 14 - 0 - 2023-03-27T13:40:16.865873300
问题分析
scheduleAtFixedRate会按固定周期尝试提交任务,若当前任务未执行完成,后续任务会被放入等待队列。当长耗时任务执行完毕后,队列中堆积的任务会被连续执行,破坏预期的执行速率。
需求是:保持「任务结束到下一个任务启动的间隔为周期减去任务执行时间」的逻辑(即任务超时则立即启动下一个),但禁止任务堆积,后续任务需按「上一个任务结束时间+周期」的节奏执行,而非一次性清空队列。
解决方案
方法1:手动递归调度(推荐)
放弃scheduleAtFixedRate,改为每次任务执行完毕后,手动计算下一次调度的延迟时间,单独调度下一次任务。这种方式不会产生任务堆积,完全符合预期节奏。
修改后的代码示例:
public static void main(String[] args) { ScheduledThreadPoolExecutor stpe = new ScheduledThreadPoolExecutor(5); AtomicInteger i = new AtomicInteger(0); long period = 1000; // 固定周期1秒 Runnable task = new Runnable() { @Override public void run() { long startTime = System.currentTimeMillis(); try { // 模拟任务执行逻辑 if (i.get() < 5) { Thread.sleep(2000); } else if (i.get() >= 15) { System.exit(0); } System.out.println(i.getAndIncrement() + " - " + stpe.getQueue().size() + " - " + LocalDateTime.now()); } catch (Exception ignored) { } finally { // 计算下次调度延迟:周期 - 任务执行耗时,若为负则立即执行(延迟0) long executionTime = System.currentTimeMillis() - startTime; long delay = Math.max(0, period - executionTime); // 递归调度下一次任务 stpe.schedule(this, delay, TimeUnit.MILLISECONDS); } } }; // 首次调度 stpe.schedule(task, 0, TimeUnit.MILLISECONDS); }
方法2:清理等待队列(不推荐)
如果坚持使用scheduleAtFixedRate,可以在每次任务执行时清理队列中堆积的重复任务,但逻辑复杂且存在线程安全风险:
- 给任务添加唯一标识;
- 每次任务执行前,加锁遍历
stpe.getQueue(),移除队列中尚未执行的同标识任务; - 该操作可能干扰调度器的正常运行,稳定性难以保证。
说明
方法1的核心是每次只调度下一次任务,由任务自身控制后续执行节奏,既实现了「超时则立即启动下一个」的逻辑,又彻底避免了任务堆积。执行后,任务5到10会按1秒间隔依次执行,而非瞬间连续输出。
内容的提问来源于stack exchange,提问作者KunLun
相关产品推荐
相关产品推荐

