Таймер Java

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

Timer и TimerTask - это служебные классы Java, используемые для планирования задач в фоновом потоке. В двух словах: 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 (delay * 2), чтобы разрешить потоку таймера выполнить задачу до того, как тест Junit перестанет выполняться.

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

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

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

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

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 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 () .

Затем миграция выполняется во время, указанное 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. С фиксированной задержкой

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

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

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

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

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

Тогда что, если мы хотим планировать эту задачу каждую секунду в режиме фиксированной задержки? Нам придется использовать перегруженную версию 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); }

This time, executions are not delayed by the previous ones:

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. Schedule a Daily Task

Next, let's run a task once a day:

@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. Cancel Timer and TimerTask

An execution of a task can be canceled in a few ways:

4.1. Cancel the TimerTask Inside Run

By calling the TimerTask.cancel() method inside the run() method's implementation of the TimerTask itself:

@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. Cancel the Timer

By calling the Timer.cancel() method on a Timer object:

@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. Stop the Thread of the TimerTask Inside Run

You can also stop the thread inside the run method of the task, thus canceling the entire task:

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

Notice the TODO instruction in the run implementation – in order to run this simple example, we'll need to actually stop the thread.

In a real-world custom thread implementation, stopping the thread should be supported, but in this case we can ignore the deprecation and use the simple stop API on the Thread class itself.

5. Timer vs ExecutorService

You can also make good use of an ExecutorService to schedule timer tasks, instead of using the timer.

Here's a quick example of how to run a repeated task at a specified interval:

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

So what are the main differences between the Timer and the ExecutorService solution:

  • Timer can be sensitive to changes in the system clock; ScheduledThreadPoolExecutor is not
  • Timer has only one execution thread; ScheduledThreadPoolExecutor can be configured with any number of threads
  • Runtime Exceptions thrown inside the TimerTask kill the thread, so following scheduled tasks won't run further; with ScheduledThreadExecutor – the current task will be canceled, but the rest will continue to run

6. Conclusion

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

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