java 单线程调度执行器或定时器调度在固定速率,但没有赶上?

qgelzfjb  于 2023-02-11  发布在  Java
关注(0)|答案(1)|浏览(175)

目前我使用的是Executors.newSingleThreadScheduledExecutor()定时调度任务,ScheduledExecutorService提供了两个选项,分别是:

  1. ScheduledExecutorService#scheduleWithFixedDelay(…)
  2. ScheduledExecutorService#scheduleAtFixedRate(…)
    一个Timer有非常相似的方法做同样的事情,问题是那些都不完全是我想要的。
    我想安排的任务通常会占用很大一部分时间,有时甚至会超出该时间(取决于当前的工作量)。
  • 我不想使用#scheduleWithFixedDelay,因为在这种情况下,任务计算时间和周期会加起来,因此周期几乎总是太长(取决于任务计算时间),即使没有超过周期。
  • 所以看起来#scheduleAtFixedRate是我想要的,但是考虑到后续几个时段任务计算时间过长的情况,或者考虑单个周期的任务计算时间为多个周期的数量级,这将导致多个周期在之后太短,因为ScheduledExecutorServiceTimer试图追赶。延迟可以无限增长,并影响以后的许多时间段,导致CPU忙碌时,它可能甚至没有必要了。

我想要的是#scheduleAtFixedRate,但是周期不应该比指定的短(它不应该试图追赶)。在下面的代码中,我想用一个例子来演示这一点。

public final class Test {
    private static int i = 0;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        System.out.println((System.currentTimeMillis() - start) + ": " + i);
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            i++;

            if (i == 1) {
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }

            System.out.println((System.currentTimeMillis() - start) + ": " + i);
        }, 0L, 1000L, TimeUnit.MILLISECONDS);
    }
}

使用#scheduleWithFixedDelay的这段代码的输出如下所示。

0: 0
5012: 1
6018: 2
7024: 3
8025: 4
9029: 5
10037: 6
11042: 7
12050: 8
...

使用#scheduleAtFixedRate的这段代码的输出如下所示。

0: 0
5024: 1
5024: 2
5024: 3
5024: 4
5025: 5
5025: 6
6025: 7
7024: 8
...

(忽略最低有效的时间数字。)我实际上想要的是这样的#schedule方法。

0: 0
5000: 1
5000: 2
6000: 3
7000: 4
8000: 5
9000: 6
10000: 7
11000: 8
...

我认为这类似于游戏循环的工作方式。问题是,Java是否有类似ScheduledExecutorServiceTimer的内置方式来调度这样的任务。如果没有,是否有简单的方法来实现它,或者是否有任何外部库可以用于此,而无需重新发明轮子?

p1tboqfb

p1tboqfb1#

事实证明,我的问题中有一个错误的假设。
Timer有非常相似的方法来做同样的事情。
事实上,这是不正确的,尽管API文档声明Timer#schedule(TimerTask, long, long)
计划指定的任务以固定延迟重复执行,
它做的事情与ScheduledThreadPoolExecutor#scheduleWithFixedDelay(…)不一样。当使用所有方法执行问题中的代码时,请参见下面的比较。

|-------|----------|----------|
|       | STPE     | Timer    |
|-------|----------|----------|
|       | 0: 0     | 0: 0     |
|       | 5012: 1  | 5007: 1  |
|       | 6018: 2  | 5007: 2  | <--
|       | 7024: 3  | 6019: 3  |
| Fixed | 8025: 4  | 7019: 4  |
| delay | 9029: 5  | 8022: 5  |
|       | 10037: 6 | 9024: 6  |
|       | 11042: 7 | 10027: 7 |
|       | 12050: 8 | 11042: 8 |
|       | ...      | ...      |
|-------|----------|----------|
|       | 0: 0     | 0: 0     |
|       | 5024: 1  | 5014: 1  |
|       | 5024: 2  | 5014: 2  |
|       | 5024: 3  | 5014: 3  |
| Fixed | 5024: 4  | 5015: 4  |
| rate  | 5025: 5  | 5015: 5  |
|       | 5025: 6  | 5015: 6  |
|       | 6025: 7  | 6002: 7  |
|       | 7024: 8  | 7002: 8  |
|       | ...      | ...      |
|-------|----------|----------|

因此,Timer#schedule(TimerTask, long, long)实际上就是我所寻找的,不同之处在于ScheduledThreadPoolExecutor#scheduleWithFixedDelay(…)在当前任务的完成和下一个任务的执行之间添加了一个固定延迟(这是不好的,因为任务计算时间总是影响有效周期),而Timer#schedule(TimerTask, long, long)在当前时间重新调度下一任务(在执行当前任务之前)+一段时间之后。(相反,固定利率方法不使用当前时间+期间,但总是计划的执行时间+一个周期。这会导致在长时间后运行时出现滴答声。)
不过,Timer#schedule(TimerTask, long, long)虽然满足了我的要求,但在我看来也不是一个理想的解决方案,最好的解决方案是只要任务计算时间不超过指定周期,就采用固定速率调度,只有超过了,后续周期才基于固定延迟调度执行,丢弃延迟。
然而,这对于JDK的ScheduledThreadPoolExecutorTimer是不可能的,因为它们都只有两个硬编码的所谓的(在其他框架中)Trigger,一个用于固定速率调度,一个用于固定延迟调度。这里是ScheduledThreadPoolExecutorTrigger,这里是TimerTrigger
对于固定速率调度,ScheduledThreadPoolExecutorTrigger例如是time += p,当它被另一个Trigger,即time = Math.max(System.nanoTime(), time + p)替换时,它正好做了我在我的问题中所要求的,即以固定速率调度而不追赶。
问题是,使用JDK的内置选项ScheduledThreadPoolExecutorTimer无法调度带有定制Trigger的任务。幸运的是,有一些开源库/框架提供了支持调度带有定制Trigger的任务的调度器。
此类库/框架的示例有WispQuartzSpring Framework。为了进行演示,我将使用Spring框架的类ThreadPoolTaskScheduler,该类具有使用自定义Trigger的方法#schedule(Runnable, Trigger)。对于标准的固定速率和固定延迟调度,已经有一个实现,即PeriodicTrigger。然而,为了以固定速率调度任务而不追赶,可以使用下面的Trigger

ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(1);
threadPoolTaskScheduler.initialize();
threadPoolTaskScheduler.schedule(task, t -> {
    Date lastExecution = t.lastScheduledExecutionTime();
    Date lastCompletion = t.lastCompletionTime();

    if (lastExecution == null || lastCompletion == null) {
        return new Date(t.getClock().millis() + delay);
    }

    return new Date(Math.max(lastCompletion.getTime(), lastExecution.getTime() + period));
});

使用此调度程序时,结果与上表中使用固定延迟调度时Timer的结果相同,不同之处在于此调度程序精确得多,因为尽可能多地使用了真正的固定速率调度。

相关问题