Таймер 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. З фіксованою затримкою

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

Отже, давайте планувати розсилку щосекунди, яка в основному є спамом, але оскільки надсилання є фальшивим, ми готові!

Давайте спочатку розробимо NewsletterTask :

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

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

Тоді, що, якщо ми хочемо запланувати це завдання щосекунди в режимі з фіксованою затримкою? Нам доведеться використовувати перевантажену версію 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. З фіксованою ставкою

А що, якби ми використовували повторення з фіксованою ставкою? Тоді ми повинні були б використати метод rasporedAtFixedRate () :

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 є набагато більш складні та повні рішення, наприклад, такі як бібліотека кварцу, - але це дуже гарне місце для початку.

Здійснення цих прикладів можна знайти в проекті GitHub - це проект на основі Eclipse, тому його слід легко імпортувати та запускати як є.