延迟执行一次任务,或者每隔一段时间执行一次,可以使用 ScheduledThreadPoolExecutor。它与普通线程池的区别,不只是任务前面多写一个 sleep。

延迟执行

import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ScheduledDemo {
    public static void main(String[] args) throws Exception {
        ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(1);
        pool.setRemoveOnCancelPolicy(true);
        try {
            ScheduledFuture<String> future = pool.schedule(
                    () -> "任务完成", 50, TimeUnit.MILLISECONDS);
            System.out.println(future.get());
        } finally {
            pool.shutdown();
        }
    }
}

到达延迟后,任务才具备执行资格,但不保证恰好在第 50 毫秒执行。系统调度、已有任务都可能继续推迟它。定时线程池不是实时调度系统。

固定频率与固定间隔

scheduleAtFixedRate 按初始计划计算每次的目标时间。例如周期为 1 秒,理论目标是第 0、1、2 秒,而不是上次执行完再算 1 秒。

scheduleWithFixedDelay 从上一次执行结束后开始计算间隔。如果任务运行 3 秒、间隔 1 秒,下一次最早要到上次开始后的约 4 秒才具备执行条件。

对于同一个周期任务,即使线程池有多个线程,也不会让两次执行相互重叠。固定频率任务落后于计划时,后续可能连续执行,但不会自动并行补跑同一个任务。

下面让第三次执行抛出异常,观察周期任务的后续行为:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class PeriodicFailureDemo {
    public static void main(String[] args) throws InterruptedException {
        ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(1);
        AtomicInteger count = new AtomicInteger();
        try {
            ScheduledFuture<?> future = pool.scheduleAtFixedRate(() -> {
                int n = count.incrementAndGet();
                System.out.println(n);
                if (n == 3) throw new IllegalStateException("模拟失败");
            }, 0, 20, TimeUnit.MILLISECONDS);
            try {
                future.get();
            } catch (ExecutionException e) {
                System.out.println("后续执行已停止");
            }
        } finally {
            pool.shutdownNow();
        }
    }
}

会打印 1、2、3,然后报告停止。周期任务异常退出后,后续执行会被抑制。要继续运行,就要在任务内部处理预期异常,同时记录失败,不能让错误无声无息地消失。

队列与关闭

ScheduledThreadPoolExecutor 内部使用延迟队列组织任务,主要依赖 corePoolSize 控制工作线程。它不是普通线程池“队列满后扩展到 maximumPoolSize”的配置方式。

取消的延迟任务默认可能等到原定延迟到期才从队列移除;setRemoveOnCancelPolicy(true) 可以尽早移除,减少大量取消任务造成的保留。

默认 shutdown 后仍执行已有的延迟任务,但不继续已有的周期任务,这两项都有对应的策略开关。与单线程 Timer 相比,线程池可以提供多个工作线程,但某个任务的异常、耗时和取消仍然要单独处理。

最后修改:2018 年 04 月 22 日
如果觉得我的文章对你有用,请随意赞赏