Рубрики
Без рубрики

Таймер Java

Используя таймер Java и TimerTask, чтобы получить максимальную отдачу от этого. Запланируйте отложенную разовую задачу, ежедневную задачу или просто повторите ее с интервалом. Холодные бобы.

Автор оригинала: Eugen Paraschiv.

1. Таймер – Основы

Timer и TimerTask – это классы java util, используемые для планирования задач в фоновом потоке. В нескольких словах – TimerTask – это задача, которую нужно выполнить, и Timer – это планировщик .

2. Запланируйте задачу один раз

2.1. После заданной задержки

Давайте начнем с простого выполнения одной задачи с помощью Таймер :

@Test
public void givenUsingTimer_whenSchedulingTaskOnce_thenCorrect() {
    TimerTask task = new TimerTask() {
        public void run() {
            System.out.println("Task performed on: " + new Date() + "n" +
              "Thread's name: " + Thread.currentThread().getName());
        }
    };
    Timer timer = new Timer("Timer");
    
    long delay = 1000L;
    timer.schedule(task, delay);
}

Теперь это выполняет задачу после определенной задержки , заданной в качестве второго параметра метода schedule () . В следующем разделе мы рассмотрим, как запланировать задачу на заданную дату и время.

Обратите внимание, что если мы запускаем этот тест JUnit, мы должны добавить Thread.sleep(задержка * 2) вызов, позволяющий потоку Таймера выполнить задачу до того, как тест Junit прекратит выполнение.

2.2. В Заданную Дату и время

Теперь давайте рассмотрим метод Timer#schedule(TimerTask, Date) , который принимает Date вместо long в качестве второго параметра, позволяя нам планировать задачу в определенный момент, а не после задержки.

На этот раз давайте представим, что у нас есть старая устаревшая база данных, и мы хотим перенести ее данные в новую базу данных с лучшей схемой.

Мы могли бы создать задачу Миграции базы данных класс, который будет обрабатывать эту миграцию:

public class DatabaseMigrationTask extends TimerTask {
    private List oldDatabase;
    private List newDatabase;

    public DatabaseMigrationTask(List oldDatabase, List newDatabase) {
        this.oldDatabase = oldDatabase;
        this.newDatabase = newDatabase;
    }

    @Override
    public void run() {
        newDatabase.addAll(oldDatabase);
    }
}

Для простоты мы представляем две базы данных с помощью List из String . Проще говоря, наша миграция состоит из переноса данных из первого списка во второй.

Чтобы выполнить эту миграцию в нужный момент, нам придется использовать перегруженную версию метода schedule () |/:

List oldDatabase = Arrays.asList("Harrison Ford", "Carrie Fisher", "Mark Hamill");
List newDatabase = new ArrayList<>();

LocalDateTime twoSecondsLater = LocalDateTime.now().plusSeconds(2);
Date twoSecondsLaterAsDate = Date.from(twoSecondsLater.atZone(ZoneId.systemDefault()).toInstant());

new Timer().schedule(new DatabaseMigrationTask(oldDatabase, newDatabase), twoSecondsLaterAsDate);

Как мы видим, мы передаем задачу миграции, а также дату выполнения в метод schedule () .

Затем миграция выполняется в момент, указанный двумя секундами позже :

while (LocalDateTime.now().isBefore(twoSecondsLater)) {
    assertThat(newDatabase).isEmpty();
    Thread.sleep(500);
}
assertThat(newDatabase).containsExactlyElementsOf(oldDatabase);

Пока мы находимся до этого момента, миграция не происходит.

3. Запланируйте Повторяемую задачу

Теперь, когда мы рассмотрели, как планировать однократное выполнение задачи, давайте посмотрим, как справляться с повторяющимися задачами.

Еще раз, есть несколько возможностей, предлагаемых классом Timer : мы можем настроить повторение для наблюдения либо фиксированной задержки, либо фиксированной скорости.

Фиксированная задержка означает, что выполнение начнется через некоторое время после момента последнего запуска выполнения, даже если оно было отложено (следовательно, само по себе задерживается) .

Допустим, мы хотим запланировать какую-то задачу каждые две секунды, и что первое выполнение занимает одну секунду, а второе-две, но задерживается на одну секунду. Затем третья казнь начнется на пятой секунде:

0s     1s    2s     3s           5s
|--T1--|
|-----2s-----|--1s--|-----T2-----|
|-----2s-----|--1s--|-----2s-----|--T3--|

С другой стороны, фиксированная ставка означает, что каждое выполнение будет соответствовать первоначальному расписанию, независимо от того, было ли предыдущее выполнение отложено .

Давайте повторим наш предыдущий пример, с фиксированной скоростью, вторая задача начнется через три секунды (из-за задержки). Но, третий через четыре секунды (соблюдая первоначальный график одного выполнения каждые две секунды):

0s     1s    2s     3s    4s
|--T1--|       
|-----2s-----|--1s--|-----T2-----|
|-----2s-----|-----2s-----|--T3--|

Эти два принципа рассматриваются, давайте посмотрим, как их использовать.

Для использования планирования с фиксированной задержкой существуют еще две перегрузки график() метод, каждый из которых принимает дополнительный параметр, указывающий периодичность в миллисекундах.

Почему две перегрузки? Потому что все еще есть возможность запустить задачу в определенный момент или после определенной задержки.

Что касается планирования с фиксированной скоростью, у нас есть два метода scheduleAtFixedRate () , которые также имеют периодичность в миллисекундах. Опять же, у нас есть один метод, чтобы запустить задачу в заданную дату и время, и другой, чтобы запустить ее после заданной задержки.

Также стоит упомянуть, что, если выполнение задачи занимает больше времени, чем период, он задерживает всю цепочку выполнения, независимо от того, используем ли мы фиксированную задержку или фиксированную скорость.

3.1. С фиксированной задержкой

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

Итак, давайте планировать рассылку каждую секунду, что в основном является спамом, но поскольку отправка поддельная, мы готовы к работе!

Давайте сначала разработаем задачу Рассылка новостей :

public class NewsletterTask extends TimerTask {
    @Override
    public void run() {
        System.out.println("Email sent at: " 
          + LocalDateTime.ofInstant(Instant.ofEpochMilli(scheduledExecutionTime()), 
          ZoneId.systemDefault()));
    }
}

Каждый раз, когда он выполняется, задача будет печатать свое запланированное время, которое мы собираем с помощью метода TimerTask#scheduledExecutionTime () .

Тогда что, если мы хотим планировать эту задачу каждую секунду в режиме фиксированной задержки? Нам придется использовать перегруженную версию schedule () , о которой мы говорили ранее:

new Timer().schedule(new NewsletterTask(), 0, 1000);

for (int i = 0; i < 3; i++) {
    Thread.sleep(1000);
}

Конечно, мы проводим тесты только для нескольких случаев:

Email sent at: 2020-01-01T10:50:30.860
Email sent at: 2020-01-01T10:50:31.860
Email sent at: 2020-01-01T10:50:32.861
Email sent at: 2020-01-01T10:50:33.861

Как мы видим, между каждым выполнением есть по крайней мере одна секунда, но иногда они задерживаются на миллисекунду. Это явление связано с нашим решением использовать повторение с фиксированной задержкой.

3.2. С Фиксированной ставкой

Теперь, что, если бы мы использовали повторение с фиксированной скоростью? Тогда нам придется использовать метод scheduleatfixedrate() :

new Timer().scheduleAtFixedRate(new NewsletterTask(), 0, 1000);

for (int i = 0; i < 3; i++) {
    Thread.sleep(1000);
}

На этот раз выполнение не задерживается предыдущими :

Email sent at: 2020-01-01T10:55:03.805
Email sent at: 2020-01-01T10:55:04.805
Email sent at: 2020-01-01T10:55:05.805
Email sent at: 2020-01-01T10:55:06.805

3.3. Запланируйте ежедневное задание

Далее, давайте запускать задачу один раз в день :

@Test
public void givenUsingTimer_whenSchedulingDailyTask_thenCorrect() {
    TimerTask repeatedTask = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
        }
    };
    Timer timer = new Timer("Timer");
    
    long delay = 1000L;
    long period = 1000L * 60L * 60L * 24L;
    timer.scheduleAtFixedRate(repeatedTask, delay, period);
}

4. Отмена таймера и таймертаска

Выполнение задачи может быть отменено несколькими способами:

4.1. Отмените Таймертаск внутри запуска

Вызывая метод TimerTask.cancel() внутри реализации метода run() самого метода TimerTask/|:

@Test
public void givenUsingTimer_whenCancelingTimerTask_thenCorrect()
  throws InterruptedException {
    TimerTask task = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
            cancel();
        }
    };
    Timer timer = new Timer("Timer");
    
    timer.scheduleAtFixedRate(task, 1000L, 1000L);
    
    Thread.sleep(1000L * 2);
}

4.2. Отмена таймера

Вызывая метод Timer.cancel() на Таймер объект:

@Test
public void givenUsingTimer_whenCancelingTimer_thenCorrect() 
  throws InterruptedException {
    TimerTask task = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
        }
    };
    Timer timer = new Timer("Timer");
    
    timer.scheduleAtFixedRate(task, 1000L, 1000L);
    
    Thread.sleep(1000L * 2); 
    timer.cancel(); 
}

4.3. Остановите поток TimerTask Внутри Run

Вы также можете остановить поток внутри метода run задачи, тем самым отменив всю задачу:

@Test
public void givenUsingTimer_whenStoppingThread_thenTimerTaskIsCancelled() 
  throws InterruptedException {
    TimerTask task = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
            // TODO: stop the thread here
        }
    };
    Timer timer = new Timer("Timer");
    
    timer.scheduleAtFixedRate(task, 1000L, 1000L);
    
    Thread.sleep(1000L * 2); 
}

Обратите внимание на инструкцию TODO в реализации run – чтобы запустить этот простой пример, нам нужно будет фактически остановить поток.

В реальной реализации пользовательского потока должна поддерживаться остановка потока, но в этом случае мы можем игнорировать устаревание и использовать простой stop API для самого класса потока.

5. Таймер против ExecutorService

Вы также можете эффективно использовать службу ExecutorService для планирования задач таймера вместо использования таймера.

Вот краткий пример того, как выполнить повторную задачу с заданным интервалом:

@Test
public void givenUsingExecutorService_whenSchedulingRepeatedTask_thenCorrect() 
  throws InterruptedException {
    TimerTask repeatedTask = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
        }
    };
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    long delay  = 1000L;
    long period = 1000L;
    executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.MILLISECONDS);
    Thread.sleep(delay + period * 3);
    executor.shutdown();
}

Итак, каковы основные различия между решением Time и решением ExecutorService :

  • Таймер может быть чувствителен к изменениям в системных часах; ScheduledThreadPoolExecutor не является
  • Таймер имеет только один поток выполнения; ScheduledThreadPoolExecutor может быть настроен с любым количеством потоков
  • Исключения времени выполнения, создаваемые внутри TimerTask , убивают поток, поэтому следующие запланированные задачи не будут выполняться дальше; с ScheduledThreadExecutor – текущая задача будет отменена, но остальные будут продолжать выполняться

6. Заключение

Этот учебник проиллюстрировал множество способов, которыми вы можете использовать простой, но гибкий Timer и TimerTask инфраструктура, встроенная в Java, для быстрого планирования задач. Конечно, в мире Java есть гораздо более сложные и полные решения, если они вам нужны, такие как библиотека Quartz , но это очень хорошее место для начала.

Реализацию этих примеров можно найти в проекте GitHub – это проект на основе Eclipse, поэтому его должно быть легко импортировать и запускать как есть.