谨慎使用 ScheduledExecutorService 执行周期性操作

问题

最近一个项目的程序需要安装到客户机器中(以服务方式),这个程序是Java 语言实现的。 这个程序大概做的事情就是每隔一段的时间从服务端拉取一些数据,经过一些计算之后再将结果发送到服务端。
然后此程序还包括一个每分钟发送一次的心跳,心跳是发送方式是HTTP GET,并且此GET 请求的报文是精简过的,使得每次心跳所发送的字节非常少。

本来这一切都很正常,不过在昨天我们做数据分析的时候发现有部分机器在掉线(一段时间没有发送心跳)复活之后会频繁发送心跳占用大量的带宽,这个频率完全超过了我们预设的每分钟一次。
通过分析后发现,机器复活之后高频发送的心跳次数正好和它掉线期间应该发送的心跳次数差不多。

经过一系列的调查,我们发现导致这个问题原因其实有两个,他们如下:

  1. ScheduledExecutorService 的调度策略
  2. 客户机操作系统有快速开机功能。

关于 快速开机 我想不必多解释,如今的 Windows 10 操作系统就可以。此功能会在操作系统关机时保存当前系统的状态,当下次开机时直接恢复以达到快速开机。

分析

我们程序中使用ScheduledExecutorService 的示例代码如下

ScheduledExecutorService s = Executors.newScheduledThreadPool(2, Thread::new);
// public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,long period, TimeUnit unit);
s.scheduleAtFixedRate(() -> {
    System.out.println(System.currentTimeMillis());
}, 0, 1, TimeUnit.MINUTES);

上面这段代码与快速开机这个功能结合会发生什么呢?(上面这段代码以Windows 服务方式运行)

假如现在时间是 00:00 (HH:mm),我们将运行这段代码的设备关机。
假如现在时间是 00:10,我们将刚才关机的设备开机。

开机后查看日志会发现 输出的毫秒数有 8~9 行差不多是一样或者相差不大的,且他们都是开机之后的时间。

再让我们来看看, scheduleAtFixedRate函数的注释

Creates and executes a periodic action that becomes enabled first after the given initial delay,
and subsequently with the given period;
that is executions will commence after initialDelay then initialDelay+period, then initialDelay + 2 * period, and so on.
If any execution of the task encounters an exception, subsequent executions are suppressed.
Otherwise, the task will only terminate via cancellation or termination of the executor.
If any execution of this task takes longer than its period,
then subsequent executions may start late, but will not concurrently execute.

最重要的是最后这句话:

If any execution of this task takes longer than its period, then subsequent executions may start late,
but will not concurrently execute.

这句话表达的白话意思就是:如果你的任务在某次执行时超时了,并且超过了任务间隔周期,那么当本次任务结束后将再次执行且没有间隔。但是这个任务不会变成多线程执行。

OK,现在差不多知道我们上面的问题是如何导致的了,让我们来理一理到底发生了一些么子。

  1. 任务执行时系统准备关机了,操作系统将所有程序挂起并将状态保存起来(当然也包括我们的服务)。
  2. 过一段时间操作系统启动了,将保存起来的状态恢复,任务从被挂起中恢复。
  3. 当此次任务执行完成之后 ScheduledExecutorService 将启动此任务的时间(关机前) time 加上 任务间隔周期 period,然后与当前时间对比发现任务已经超时了,所以继续执行下一次。

精简一下ScheduledExecutorService的调度示例代码大约如下:

final long period = TimeUnit.MINUTES.toMillis(1);
final Runnable runnable = () -> { /*do something*/ };
long time = System.currentTimeMillis();
for (; ; ) {
    runnable.run();
    long now = System.currentTimeMillis();
    time += period; // next run time
    if (now < time) {
        Thread.sleep(time - now);
    }
}

具体实现细节请参考 Java 源码, 核心类是 java.util.concurrent.ScheduledThreadPoolExecutor. 当然具体源码可能与我的描述有出入,不过大致上是这样。

结语

所以,在使用 ScheduledExecutorService 一定要注意场景,还有要先看文档并且弄懂代码是如何实现的。
出现此问题的主要原因是我们在写代码之前没有仔细看文档和不了解 Java 系统库的食用姿势。
最后我们的解决方案是 自己写 while 循环执行后睡眠,不再使用任何其他框架或内库来执行周期性操作(心累)

ScheduledExecutorService是一个用于延时执行任务的线程池。它可以在指定的延迟时间后执行任务,也可以按照固定的时间间隔重复执行任务。下面是两使用ScheduledExecutorService延时执行任务的例子: 1. 使用ScheduledExecutorService执行次延时任务: ```java import java.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class DelayedTaskExample { public static void main(String[] args) { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); Runnable task = () -> { // 执行需要延时执行的任务 System.out.println("Delayed task executed."); }; int delay = 5; // 延迟时间为5秒 executor.schedule(task, delay, TimeUnit.SECONDS); executor.shutdown(); } } ``` 2. 使用ScheduledExecutorService重复执行延时任务: ```java import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class RepeatedTaskExample { public static void main(String[] args) { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); Runnable task = () -> { // 执行需要重复执行的任务 System.out.println("Repeated task executed."); }; int initialDelay = 2; // 初始延迟时间为2秒 int period = 3; // 重复执行的时间间隔为3秒 executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS); //executor.shutdown(); } } ``` 这两个例子分别展示了使用ScheduledExecutorService执行一次延时任务和重复执行延时任务的方法。你可以根据自己的需求选择适合的方式来延时执行任务。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值