目前我使用的是Executors.newSingleThreadScheduledExecutor()
定时调度任务,ScheduledExecutorService
提供了两个选项,分别是:
ScheduledExecutorService#scheduleWithFixedDelay(…)
ScheduledExecutorService#scheduleAtFixedRate(…)
一个Timer
有非常相似的方法做同样的事情,问题是那些都不完全是我想要的。
我想安排的任务通常会占用很大一部分时间,有时甚至会超出该时间(取决于当前的工作量)。
- 我不想使用
#scheduleWithFixedDelay
,因为在这种情况下,任务计算时间和周期会加起来,因此周期几乎总是太长(取决于任务计算时间),即使没有超过周期。 - 所以看起来
#scheduleAtFixedRate
是我想要的,但是考虑到后续几个时段任务计算时间过长的情况,或者考虑单个周期的任务计算时间为多个周期的数量级,这将导致多个周期在之后太短,因为ScheduledExecutorService
或Timer
试图追赶。延迟可以无限增长,并影响以后的许多时间段,导致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是否有类似ScheduledExecutorService
或Timer
的内置方式来调度这样的任务。如果没有,是否有简单的方法来实现它,或者是否有任何外部库可以用于此,而无需重新发明轮子?
1条答案
按热度按时间p1tboqfb1#
事实证明,我的问题中有一个错误的假设。
Timer
有非常相似的方法来做同样的事情。事实上,这是不正确的,尽管API文档声明
Timer#schedule(TimerTask, long, long)
计划指定的任务以固定延迟重复执行,
它做的事情与
ScheduledThreadPoolExecutor#scheduleWithFixedDelay(…)
不一样。当使用所有方法执行问题中的代码时,请参见下面的比较。因此,
Timer#schedule(TimerTask, long, long)
实际上就是我所寻找的,不同之处在于ScheduledThreadPoolExecutor#scheduleWithFixedDelay(…)
在当前任务的完成和下一个任务的执行之间添加了一个固定延迟(这是不好的,因为任务计算时间总是影响有效周期),而Timer#schedule(TimerTask, long, long)
在当前时间重新调度下一任务(在执行当前任务之前)+一段时间之后。(相反,固定利率方法不使用当前时间+期间,但总是计划的执行时间+一个周期。这会导致在长时间后运行时出现滴答声。)不过,
Timer#schedule(TimerTask, long, long)
虽然满足了我的要求,但在我看来也不是一个理想的解决方案,最好的解决方案是只要任务计算时间不超过指定周期,就采用固定速率调度,只有超过了,后续周期才基于固定延迟调度执行,丢弃延迟。然而,这对于JDK的
ScheduledThreadPoolExecutor
和Timer
是不可能的,因为它们都只有两个硬编码的所谓的(在其他框架中)Trigger
,一个用于固定速率调度,一个用于固定延迟调度。这里是ScheduledThreadPoolExecutor
的Trigger
,这里是Timer
的Trigger
。对于固定速率调度,
ScheduledThreadPoolExecutor
的Trigger
例如是time += p
,当它被另一个Trigger
,即time = Math.max(System.nanoTime(), time + p)
替换时,它正好做了我在我的问题中所要求的,即以固定速率调度而不追赶。问题是,使用JDK的内置选项
ScheduledThreadPoolExecutor
和Timer
无法调度带有定制Trigger
的任务。幸运的是,有一些开源库/框架提供了支持调度带有定制Trigger
的任务的调度器。此类库/框架的示例有Wisp、Quartz和Spring Framework。为了进行演示,我将使用Spring框架的类
ThreadPoolTaskScheduler
,该类具有使用自定义Trigger
的方法#schedule(Runnable, Trigger)
。对于标准的固定速率和固定延迟调度,已经有一个实现,即PeriodicTrigger
。然而,为了以固定速率调度任务而不追赶,可以使用下面的Trigger
。使用此调度程序时,结果与上表中使用固定延迟调度时
Timer
的结果相同,不同之处在于此调度程序精确得多,因为尽可能多地使用了真正的固定速率调度。