Перейти к основному содержимому

Java-таймер

· 8 мин. чтения

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

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

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

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

Давайте начнем с простого запуска одной задачи с помощью Timer :

@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(delay * 2) , чтобы позволить потоку таймера запустить задачу до того, как тест Junit перестанет выполняться.

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

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

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

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

public class DatabaseMigrationTask extends TimerTask {
private List<String> oldDatabase;
private List<String> newDatabase;

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

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

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

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

List<String> oldDatabase = Arrays.asList("Harrison Ford", "Carrie Fisher", "Mark Hamill");
List<String> 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() .

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

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--|

Разобравшись с этими двумя принципами, давайте посмотрим, как их использовать.

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

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

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

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

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

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

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

Давайте сначала создадим NewsletterTask :

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. Отменить таймер и TimerTask

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

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

Вызывая метод 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() для объекта Timer :

@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 Inside 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 в реализации запуска — чтобы запустить этот простой пример, нам нужно фактически остановить поток.

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

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();
}

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

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

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

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

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