Java таймер

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

Тогава, какво, ако искаме да планираме тази задача всяка секунда в режим с фиксирано закъснение? Ще трябва да използваме претоварената версия на график (), за която говорихме по-рано:

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, така че трябва да е лесно да се импортира и да се изпълнява както е.