Java ScheduledExecutorService任务的运行晚于预期

Java ScheduledExecutorService任务的运行晚于预期,java,executorservice,schedule,Java,Executorservice,Schedule,我定期运行任务,为了提供时间间隔的灵活性,下一个超时将在每个任务结束时计算,从Instant.now()转换为毫秒,并使用ScheduledExecutorService\schedule进行调度 这段代码通常运行良好(左边的蓝色曲线),但其他日子不太好 在我看来,有时启动时会出现问题(机器每晚都会重新启动),尽管程序应该而且确实会自我纠正ScheduledExecutorService#schedule不会恢复,并且计划的任务一直运行得很晚。看来,完全重启JVM是唯一的解决方案 我最初的想法

我定期运行任务,为了提供时间间隔的灵活性,下一个超时将在每个任务结束时计算,从Instant.now()转换为毫秒,并使用
ScheduledExecutorService\schedule
进行调度

这段代码通常运行良好(左边的蓝色曲线),但其他日子不太好

在我看来,有时启动时会出现问题(机器每晚都会重新启动),尽管程序应该而且确实会自我纠正
ScheduledExecutorService#schedule
不会恢复,并且计划的任务一直运行得很晚。看来,完全重启JVM是唯一的解决方案

我最初的想法是,这是一个bug,根据机器启动的时间,可能会出问题。但以下日志输出表明问题与我使用的
ScheduledExecutorService#schedule
有关:

// Log time in GMT+2, other times are in GMT
// The following lines are written following system startup (all times are correct)
08 juin 00:08:49.993 [main] WARN  com.pgscada.webdyn.Webdyn - Scheduling next webdyn service time. Currently 2018-06-07T22:08:49.993Z, last connection null
08 juin 00:08:50.586 [main] INFO  com.pgscada.webdyn.Webdyn - The next data sample at 2018-06-07T22:10:00Z and the next FTP connection at 2018-06-07T22:30:00Z
08 juin 00:08:50.586 [main] WARN  com.pgscada.webdyn.Webdyn - Completed webdyn schedule in 9ms, next execution at 2018-06-07T22:10:00Z (in 69414 ms) will run as data-sample
// So we are expecting the next execution to occur at 00:10:00 (or in 69.4 seconds)
// Except that it runs at 00:11:21
08 juin 00:11:21.206 [pool-1-thread-4] INFO  com.pgscada.webdyn.Webdyn - Executing Webdyn service, isDataSample=true, isFtpConnection=false, nextTimeout=2018-06-07T22:10:00Z, lastFtpConnection=null
// But thats OK because it should correct itself
08 juin 00:13:04.151 [pool-1-thread-4] WARN  com.pgscada.webdyn.Webdyn - Scheduling next webdyn service time. Currently 2018-06-07T22:10:00Z, last connection null
08 juin 00:13:04.167 [pool-1-thread-4] INFO  com.pgscada.webdyn.Webdyn - The next data sample at 2018-06-07T22:20:00Z and the next FTP connection at 2018-06-07T22:30:00Z
08 juin 00:13:04.167 [pool-1-thread-4] WARN  com.pgscada.webdyn.Webdyn - Completed webdyn schedule in 0ms, next execution at 2018-06-07T22:20:00Z (in 415833 ms) will run as data-sample
// So now we are expecting the next execution to occur at 00:20:00 (or in 415.8 seconds)
// But it runs at 00:28:06
08 juin 00:28:06.145 [pool-1-thread-4] INFO  com.pgscada.webdyn.Webdyn - Executing Webdyn service, isDataSample=true, isFtpConnection=false, nextTimeout=2018-06-07T22:20:00Z, lastFtpConnection=null
下面是调度功能的实际生产代码

ScheduledExecutorService EXECUTORS = Executors.newScheduledThreadPool(10);


private void scheduleNextTimeout(Instant currentTime, Instant lastFtpConnection) {

    try {

        log.info("Scheduling next webdyn service time. Currently {}, last connection {}", currentTime, lastFtpConnection);

        // Parse config files first
        getConfigIni().parse();

        long time = System.nanoTime();
        final Instant earliestPossibleTimeout = Instant.now().plusSeconds(5); 

        Instant nextDataSample = nextTimeout(currentTime);

        if (nextDataSample.isBefore(earliestPossibleTimeout)) {
            final Instant oldTime = nextDataSample;
            nextDataSample = nextTimeout(earliestPossibleTimeout);
            log.warn("Next data sample was calculated to a time in the past '{}', resetting to a future time: {}", oldTime, nextDataSample);
        }

        Instant nextFtp = nextFtpConnection(currentTime, lastFtpConnection);

        if (nextFtp.isBefore(earliestPossibleTimeout)) {
            final Instant oldTime = nextFtp;
            nextFtp = nextFtpConnection(earliestPossibleTimeout, lastFtpConnection);
            log.warn("Next FTP connection was calculated to a time in the past '{}', resetting to a future time: {}", oldTime, nextFtp);
        }

        final boolean isFtpConnection = !nextDataSample.isBefore(nextFtp);
        final boolean isDataSample = !isFtpConnection || nextDataSample.equals(nextFtp);
        log.info("The next data sample at {} and the next FTP connection at {}", nextDataSample, nextFtp);

        final Instant nextTimeout = nextDataSample.isBefore(nextFtp) ? nextDataSample : nextFtp;
        final long millis = Duration.between(Instant.now(), nextTimeout).toMillis();
        EXECUTORS.schedule(() -> {
            log.info("Executing Webdyn service, isDataSample={}, isFtpConnection={}, nextTimeout={}, lastFtpConnection={}",
                    isDataSample, isFtpConnection, nextTimeout, lastFtpConnection);

            long tme = System.nanoTime();
            try {
                connect(isDataSample, isFtpConnection, nextTimeout, lastFtpConnection);
                log.warn("Completed webdyn service in {}s", (System.nanoTime() - tme) / 1000000);
            } catch (final Throwable ex) {
                log.error("Failed webdyn service after {}ms : {}", (System.nanoTime() - tme) / 1000000, ex.getMessage(), ex);
            } finally {
                scheduleNextTimeout(nextTimeout, isFtpConnection ? nextTimeout : lastFtpConnection);
            }
        }, millis, TimeUnit.MILLISECONDS);

        log.warn("Completed webdyn schedule in {}ms, next execution at {} (in {} ms) will run as {}",
                (System.nanoTime() - time) / 1000000, nextTimeout, millis, isFtpConnection ? "ftp-connection" : "data-sample");

    } catch (final Throwable ex) {
        log.error("Fatal error in webdyn schedule : {}", ex.getMessage(), ex);
    }
}

正如我在问题下方的评论中所述,这里的问题是存在一个共享的、可变的、非线程安全的资源(EXECUTORS atribute),它被多个线程更改。 它在开始时由主线程和池中用于执行任务的线程进行更改

需要注意的是,即使一次只有一个线程访问一个共享资源(仅仅因为一次只运行一个任务),你仍然需要考虑一致性。这是因为没有同步,Java内存模型不能保证一个线程所做的更改对其他线程可见,无论它们运行多久

因此,解决方案是使方法scheduleNextTimeout保持同步,从而保证更改不会保持在执行线程的本地并写入主存

您还可以在部件周围创建一个同步块(在“this”上同步),以访问共享资源,但由于系统似乎不是重型系统,其余代码似乎不需要花费很长时间,因此不需要这样做

当我第一次面对这类问题时,我曾经从一篇又好又短的文章中学到了它的要点:)


我很高兴能提供帮助。

所以scheduleNextTimeout方法是从不同的线程执行的(因为每个任务在结束时调用它以产生新的调度),对吗?它通过调用EXECUTORS.schedule来改变EXECUTORS的状态。所以您已经从多个线程更改了共享资源(执行器),如果它不是线程安全的,那么可能会发生奇怪的事情。如果Executors.newScheduledThreadPool(10)提供的实现不是线程安全的,则可能会遇到问题。所以我试着从同步块调用EXECUTORS.schedule。也许只是让整个scheduleNextTimeout同步开始。希望有帮助。谢谢。我会试试的。该函数从两个线程执行——主线程(在系统启动时)和它自己的线程(每次执行后)。我将线程池增加到10,这样我就可以确定在每个超时时都会有一个空闲线程,但这是不必要的,而且问题似乎出在其他地方。即使是这两个线程也绝对足以解决此类问题(“我去过那里,做到了,把我的头发扯下来了”)-这是因为在一个线程中所做的某些更改可能永远不会对另一个线程可见,无论它运行多久,如果同步没有正确完成。即使在任何给定时间只有一个线程访问共享资源,您仍然会遇到一致性问题。所以我的赌注仍然在这类问题上,但肯定的是——我可能错了。如果我的建议行得通的话,我稍后会发布正确的答案,也许会有一些链接。这些图表到底是什么意思?一个是绘制千瓦与时间的关系图,另一个是W/m^2。这和你问的问题有什么关系?这个网站真的不适用于删除已解决问题的人:)我会给出一个正确的答案,如果你接受,我会很高兴的。