Расписание ScheduledExecutorService не запускается

Следующий блок кода у меня не работает (оповещение не срабатывает):

public static void main(String[] args) throws InterruptedException, ParseException {        
    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(Thread::new);
    TimeZone timeZone = TimeZone.getTimeZone(ZoneId.systemDefault());
    Calendar calendar = Calendar.getInstance(timeZone);

    Scanner scanner = new Scanner(System.in);
    System.out.println("The time now is: " + calendar.getTime());
    System.out.println("Enter alert date time: ");
    String dateStr = scanner.nextLine();
    SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
    Date date = sdf.parse(dateStr);
    calendar.setTime(date);
    long alertTimeInMillis = calendar.getTimeInMillis();
    long now = Calendar.getInstance(timeZone).getTimeInMillis();
    System.out.println("Time to alert: " + (alertTimeInMillis - now) + " millis ");

    ScheduledFuture<?> scheduledFuture = scheduledExecutorService.schedule(() -> System.out.println("alert!")
            , alertTimeInMillis, TimeUnit.MILLISECONDS);
    
    while (!scheduledFuture.isDone()) {
        System.out.println("The time now: " + Calendar.getInstance(timeZone).getTime());
        System.out.println("Expected alert time: " + date);
        Thread.sleep(1000);
    }
    scheduledExecutorService.shutdown();
    scheduledExecutorService.awaitTermination(30, TimeUnit.SECONDS);
}

Пока этот блок кода работает:

public static void main(String[] args) throws InterruptedException {

    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(Thread::new);
    LocalDateTime localDateTime = LocalDateTime.of(2023, 1, 10, 12, 1);
    ScheduledFuture<?> scheduledFuture = scheduledExecutorService.schedule(() ->
            System.out.println("alert!"),
            LocalDateTime.now().until(localDateTime, ChronoUnit.SECONDS), TimeUnit.SECONDS);

    while (!scheduledFuture.isDone()) {
        Thread.sleep(1000);
    }
    scheduledExecutorService.shutdown();
    scheduledExecutorService.awaitTermination(30, TimeUnit.SECONDS);
}

Я не понимаю разницы или что именно не так с первым блоком, который мне не хватает.

Кажется, у вас неправильное представление в первом фрагменте: вы печатаете (alertTimeInMillis - now) как время оповещения, но передаете alertTimeInMillis только schedule(...). Если вы прочитаете JavaDoc на Calendar.getTimeInMillis(), вы увидите, что это количество миллисекунд с начала эпохи (т.е. 01.01.1970 00:00:00,000 UTC), и если вы задержитесь на это количество миллисекунд, вы будете ждать еще 53 года. для предупреждения :) - Кстати, второй фрагмент в любом случае лучше, поскольку он отказывается от старого java.util.Date API.

Thomas 10.01.2023 11:10

Я понял, спасибо! Причина, по которой я использую старый Date, заключается в том, что я пытаюсь помочь кому-то с упражнением, которое требует от него работы (к сожалению :).

Nom1fan 10.01.2023 13:39

Это действительно очень жаль. Никто не должен учиться использовать хлопотные и давно устаревшие классы Date, Calendar, SimpleDateFormat и TimZone.

Ole V.V. 10.01.2023 15:33
Лучшая компания по разработке спортивных приложений
Лучшая компания по разработке спортивных приложений
Ищете лучшую компанию по разработке спортивных приложений? Этот список, несомненно, облегчит вашу работу!
Blibli Automation Journey - Как захватить сетевой трафик с помощью утилиты HAR в Selenium 4
Blibli Automation Journey - Как захватить сетевой трафик с помощью утилиты HAR в Selenium 4
Если вы являетесь веб-разработчиком или тестировщиком, вы можете быть знакомы с Selenium, популярным инструментом для автоматизации работы...
Фото ️🔁 Radek Jedynak 🔃 on ️🔁 Unsplash 🔃
Фото ️🔁 Radek Jedynak 🔃 on ️🔁 Unsplash 🔃
Что такое Java 8 Streams API? Java 8 Stream API
Деревья поиска (Алгоритм4 Заметки к учебнику)
Деревья поиска (Алгоритм4 Заметки к учебнику)
(1) Двоичные деревья поиска: среднее lgN, наихудшее N для вставки и поиска.
2
3
64
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Отладка устаревшего кода

ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(Thread::new);

В этом нет необходимости Thread::new. Используйте пустые скобки, аргументы не нужны.

TimeZone timeZone = TimeZone.getTimeZone(ZoneId.systemDefault());

Странный. Вы смешиваете устаревший класс TimeZone с его заменой ZoneId из java.time. Используйте только java.time; никогда не используйте ужасно ошибочные устаревшие классы даты и времени.

ZoneId z = ZoneId.sytemDefault() ;

К старым классам были добавлены новые методы преобразования для написания кода для взаимодействия со старым кодом, еще не обновленным до java.time. TimeZone.getTimeZone( ZoneId ) — один из таких методов преобразования.

Также имейте в виду, что текущий часовой пояс JVM по умолчанию может быть изменен в любой момент любым кодом в любом потоке. Подумайте, действительно ли вы хотите, чтобы ваш код крутил колесо в рулетке часовых поясов во время выполнения.

new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");

Ваш код форматирования использует hh в нижнем регистре. Это означает 12-часовой формат времени. Но вы забыли собрать индикатор какой половины дня. Если пользователь вводит время для 02:00:00, мы не можем знать, имел ли он в виду 2 часа ночи или 14 часов. Я изменю это на HH для 24-часового формата в примере кода ниже.

SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss"); Date date = sdf.parse(dateStr);

Имейте в виду, что SimpleDateFormat использует часовой пояс по умолчанию для анализа этой строки даты и времени. Вы хотите использовать часовой пояс по умолчанию в своем предыдущем коде, так что это может сработать. Или это может не сработать, если какой-то код тем временем изменил текущий часовой пояс вашей JVM по умолчанию.

ScheduledFuture<?> scheduledFuture = scheduledExecutorService.schedule(() -> System.out.println("alert!") , alertTimeInMillis, TimeUnit.MILLISECONDS);

👉 Эта строка является источником вашей проблемы. Вы сказали запланированной службе-исполнителю подождать несколько миллисекунд перед выполнением. Ваше количество исполнителей — это количество миллисекунд с момента отсчета эпохи первого момента 1970 года в UTC, то есть десятилетий. Вы сказали службе-исполнителю ждать десятилетия, примерно 53 года (2023-1970=53). Что и сделает служба-исполнитель, если вы оставите свой компьютер включенным так долго.

Ваш код:

long alertTimeInMillis = calendar.getTimeInMillis();

… вызывает метод Calendar#getTimeInMillis. Javadoc говорит:

Возвращает: текущее время в миллисекундах UTC от эпохи.

То, что вы хотели сделать, это то, что вы сделали в своем System.out.println: рассчитать время, прошедшее между текущим моментом и этим целевым моментом.

(alertTimeInMillis - now)

Совет. Как правило, лучше регистрировать существующие значения, а не создавать значение для регистрации.

Современное решение: java.time

Но хватит бороться с ужасно несовершенными устаревшими классами даты и времени. Никогда не используйте Calendar, Date или SimpleDateFormat. Всегда используйте классы java.time.

Во-первых, если вы используете службу-исполнитель, скопируйте шаблонный код для корректного завершения работы. Найдите этот код в ExecutorService Javadoc. Вот немного измененная версия.

package work.basil.example.time;

import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

public class WaitToRunDemo
{
    public static void main ( String[] args )
    {
        WaitToRunDemo app = new WaitToRunDemo();
        app.demo();
    }

    private void demo ( )
    {
    }

    // My slightly modified version of boilerplate code taken from Javadoc of `ExecutorService`.
    // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html 
    void shutdownAndAwaitTermination ( final ExecutorService executorService , final Duration waitForTasksToWork , final Duration waitForTasksToComplete )
    {
        executorService.shutdown(); // Disable new tasks from being submitted
        try
        {
            // Wait a while for existing tasks to terminate
            if ( ! executorService.awaitTermination( waitForTasksToWork.toMillis() , TimeUnit.MILLISECONDS ) )
            {
                executorService.shutdownNow(); // Cancel currently executing tasks
                // Wait a while for tasks to respond to being cancelled
                if ( ! executorService.awaitTermination( waitForTasksToComplete.toMillis() , TimeUnit.MILLISECONDS ) )
                { System.err.println( "ExecutorService did not terminate." ); }
            }
        }
        catch ( InterruptedException ex )
        {
            // (Re-)Cancel if current thread also interrupted
            executorService.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }
    }
}

Вот полный пример кода.

Давайте разобьем ваш длинный код на несколько отдельных методов, каждый из которых преследует определенную цель.

Чтобы представить промежуток времени, не привязанный к временной шкале, используйте класс Duration.

Обратите внимание, как мы свели к минимуму использование LocalDateTime. Этот класс не может представлять момент, точку на временной шкале, поскольку ему не хватает контекста часового пояса или смещения от UTC. Не могу представить ситуацию, когда звонок LocalDateTime.now оптимален.

package work.basil.example.time;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class WaitToRunDemo
{
    public static void main ( String[] args )
    {
        WaitToRunDemo app = new WaitToRunDemo();
        app.demo();
    }

    private void demo ( )
    {
        ZonedDateTime then = this.gatherInput();
        Duration delayUntilTaskRuns = this.calculateTimeToElapse( then );
        System.out.println( "INFO - Now: " + Instant.now() + ". Scheduling task to run after delay of: " + delayUntilTaskRuns.toString() );

        this.runTask( delayUntilTaskRuns );
        System.out.println( "Demo done at " + Instant.now() );
    }

    private ZonedDateTime gatherInput ( )
    {
        // Put your `Scanner` code here. Tip: Confirm the time zone with the user.

        // Simulate the user entering text for the start of the next minute.
        ZoneId zoneId = ZoneId.systemDefault();
        ZonedDateTime now = ZonedDateTime.now( zoneId );
        ZonedDateTime startOfNextMinute = now.truncatedTo( ChronoUnit.MINUTES ).plusMinutes( 1 );

        DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd-MM-uuuu HH:mm:ss" );
        String userInput = startOfNextMinute.format( f );
        System.out.println( "DEBUG startOfNextMinute = " + startOfNextMinute );
        System.out.println( "DEBUG userInput = " + userInput );

        // Parse user input text into a `LocalDateTime` object to represent date with time-of-day but lacking any time zone or offset-from-UTC. 
        LocalDateTime ldt = LocalDateTime.parse( userInput , f );
        ZonedDateTime zdt = ldt.atZone( zoneId );

        return zdt;
    }

    private Duration calculateTimeToElapse ( final ZonedDateTime then )
    {
        Instant now = Instant.now();
        Duration delayUntilTaskRuns = Duration.between( now , then.toInstant() );
        if ( delayUntilTaskRuns.isNegative() ) { throw new IllegalStateException( "Specified wait time is negative (in the past)." ); }
        return delayUntilTaskRuns;
    }

    private void runTask ( Duration delay )
    {
        ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
        Runnable task = ( ) -> System.out.println( "Done running task at " + Instant.now() );
        ses.schedule( task , delay.toMillis() , TimeUnit.MILLISECONDS );
        this.shutdownAndAwaitTermination( ses , Duration.ofMinutes( 2 ) , Duration.ofMinutes( 1 ) );
    }

    // My slightly modified version of boilerplate code taken from Javadoc of `ExecutorService`.
    // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html
    void shutdownAndAwaitTermination ( final ExecutorService executorService , final Duration waitForTasksToWork , final Duration waitForTasksToComplete )
    {
        executorService.shutdown(); // Disable new tasks from being submitted
        try
        {
            // Wait a while for existing tasks to terminate
            if ( ! executorService.awaitTermination( waitForTasksToWork.toMillis() , TimeUnit.MILLISECONDS ) )
            {
                executorService.shutdownNow(); // Cancel currently executing tasks
                // Wait a while for tasks to respond to being cancelled
                if ( ! executorService.awaitTermination( waitForTasksToComplete.toMillis() , TimeUnit.MILLISECONDS ) )
                { System.err.println( "ExecutorService did not terminate." ); }
            }
        }
        catch ( InterruptedException ex )
        {
            // (Re-)Cancel if current thread also interrupted
            executorService.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }
    }
}

Где запустить:

DEBUG startOfNextMinute = 2023-01-10T14:30-08:00[America/Los_Angeles]
DEBUG userInput = 10-01-2023 14:30:00
INFO - Now: 2023-01-10T22:29:37.091459Z. Scheduling task to run after delay of: PT22.908594S
Done running task at 2023-01-10T22:30:00.012244Z
Demo done at 2023-01-10T22:30:00.024242Z

Мой пример кода при запуске был нацелен на запуск задачи в 2023-01-10T14:30-08:00[Америка/Лос-Анджелес]. Это тот же самый момент, та же точка на временной шкале, что и 2023-01-10T22:30:00Z, где «Z» означает смещение от UTC на ноль часов-минут-секунд. 14:30 в Америке/Лос-Анджелесе на 8 часов отстает от UTC, поэтому добавление 8 часов приводит к 22:30 по UTC — тот же момент, другое время настенных часов.

Другими словами… Алиса в Портленде, штат Орегон, замечает, что ее настенные часы бьют 14:30 (14:30), когда она набирает номер, чтобы вызвать Бобу в Рейкьявик . Когда Боб отвечает на звонок, он замечает свои собственные часы на стене, которые показывают 22:30 (22:30). Тот же самый момент, другое время настенных часов.


Кстати, имейте в виду, что строки, отправленные в System.out.println из разных тем, могут отображаться в консоли не в хронологическом порядке. Всегда указывайте временную метку, например Instant.now(). Если вам важна последовательность, изучите эти метки времени.

Большое спасибо за подробный ответ и обучение меня за рамками, как в ожидании прекращения! Несколько вопросов: 1) Вы сказали, что это странно, что я смешиваю устаревший код с современным кодом, но TimeZone#getTimeZone принимает ZoneId, разве это не смешано по дизайну? 2) Меня смущают отпечатки, так как пользовательский ввод 14:30, но он срабатывает в 22:30 ваше время. Это проблема часового пояса? Еще раз спасибо!

Nom1fan 11.01.2023 09:28

@ Nom1fan В старые классы были добавлены новые методы преобразования для написания кода для взаимодействия со старым кодом, еще не обновленным до java.time. TimeZone.getTimeZone( ZoneId ) — один из таких методов преобразования.

Basil Bourque 11.01.2023 09:52

Мой пример кода при запуске был нацелен на запуск задачи в 2023-01-10T14:30-08:00[Америка/Лос-Анджелес]. Это тот же самый момент, та же точка на временной шкале, что и 2023-01-10T22:30:00Z, где «Z» означает смещение от UTC на ноль часов-минут-секунд. 14:30 в America/Los_Angeles на 8 часов отстает от UTC, поэтому добавление 8 часов дает вам 22:30 в UTC.

Basil Bourque 11.01.2023 09:56

Я немного изменил современный ответ Бэзила, чтобы отпечатки находились в одном часовом поясе и имели для меня больше смысла. Не стесняйтесь поправлять меня, если я сделал что-то не так.

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class WaitToRunDemo {
    public static void main(String[] args) {
        WaitToRunDemo app = new WaitToRunDemo();
        app.demo();
    }

    private void demo() {
        ZoneId zoneId = ZoneId.systemDefault();
        ZonedDateTime now = ZonedDateTime.now(zoneId);
        ZonedDateTime then = this.gatherInput(now, zoneId);
        Duration delayUntilTaskRuns = this.calculateTimeToElapse(now, then);
        System.out.println("INFO - Now: " + now + ". Scheduling task to run after delay of: " + delayUntilTaskRuns);

        this.runTask(delayUntilTaskRuns, zoneId);
        System.out.println("Demo done at: " + ZonedDateTime.now(zoneId));
    }

    private ZonedDateTime gatherInput(ZonedDateTime now, ZoneId zoneId) {
        // Put your `Scanner` code here. Tip: Confirm the time zone with the user.

        // Simulate the user entering text for the start of the next minute.
        ZonedDateTime startOfNextMinute = now.truncatedTo(ChronoUnit.MINUTES).plusMinutes(1);

        DateTimeFormatter f = DateTimeFormatter.ofPattern("dd-MM-uuuu HH:mm:ss");
        String userInput = startOfNextMinute.format(f);
        System.out.println("DEBUG startOfNextMinute = " + startOfNextMinute);
        System.out.println("DEBUG userInput = " + userInput);

        // Parse user input text into a `LocalDateTime` object to represent date with time-of-day but lacking any time zone or offset-from-UTC.
        LocalDateTime ldt = LocalDateTime.parse(userInput, f);

        return ldt.atZone(zoneId);
    }

    private Duration calculateTimeToElapse(final ZonedDateTime now, final ZonedDateTime then) {
        Duration delayUntilTaskRuns = Duration.between(now, then);
        if (delayUntilTaskRuns.isNegative()) {
            throw new IllegalStateException("Specified wait time is negative (in the past).");
        }
        return delayUntilTaskRuns;
    }

    private void runTask(Duration delay, ZoneId zoneId) {
        ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
        Runnable task = () -> System.out.println("Done running task at: " + ZonedDateTime.now(zoneId));
        ses.schedule(task, delay.toMillis(), TimeUnit.MILLISECONDS);
        this.shutdownAndAwaitTermination(ses, Duration.ofMinutes(2), Duration.ofMinutes(1));
    }

    // My slightly modified version of boilerplate code taken from Javadoc of `ExecutorService`.
    // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html
    void shutdownAndAwaitTermination(final ExecutorService executorService, final Duration waitForTasksToWork, final Duration waitForTasksToComplete) {
        executorService.shutdown(); // Disable new tasks from being submitted
        try {
            // Wait a while for existing tasks to terminate
            if (!executorService.awaitTermination(waitForTasksToWork.toMillis(), TimeUnit.MILLISECONDS)) {
                executorService.shutdownNow(); // Cancel currently executing tasks
                // Wait a while for tasks to respond to being cancelled
                if (!executorService.awaitTermination(waitForTasksToComplete.toMillis(), TimeUnit.MILLISECONDS)) {
                    System.err.println("ExecutorService did not terminate.");
                }
            }
        } catch (InterruptedException ex) {
            // (Re-)Cancel if current thread also interrupted
            executorService.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }
    }
}

Выход:

DEBUG startOfNextMinute = 2023-01-11T10:55+02:00[Asia/Jerusalem]
DEBUG userInput = 11-01-2023 10:55:00
INFO - Now: 2023-01-11T10:54:45.494618+02:00[Asia/Jerusalem]. Scheduling task to run after delay of: PT14.505382S
Done running task at: 2023-01-11T10:55:00.036986+02:00[Asia/Jerusalem]
Demo done at: 2023-01-11T10:55:00.037542+02:00[Asia/Jerusalem]

Другие вопросы по теме