Java Timer

1. Timer - podstawy

Timer i TimerTask to klasy wykorzystujące java używane do planowania zadań w wątku w tle. W kilku słowach - TimerTask to zadanie do wykonania, a Timer to harmonogram .

2. Zaplanuj zadanie raz

2.1. Po określonym opóźnieniu

Zacznijmy od uruchomienia pojedynczego zadania za pomocą timera :

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

Teraz to wykonuje zadanie po pewnym opóźnieniu , podanym jako drugi parametr metody schedule () . W następnej sekcji zobaczymy, jak zaplanować zadanie na określony dzień i godzinę.

Zauważ, że jeśli uruchamiamy ten test JUnit, powinniśmy dodać wywołanie Thread.sleep (delay * 2), aby umożliwić wątkowi Timera uruchomienie zadania, zanim test Junit przestanie działać.

2.2. W określonym dniu i czasie

Teraz zobaczmy metodę Timer # schedule (TimerTask, Date) , która przyjmuje Date zamiast długości drugiego parametru, co pozwala nam zaplanować zadanie w określonym momencie, a nie po opóźnieniu.

Tym razem wyobraźmy sobie, że mamy starą starszą bazę danych i chcemy przenieść jej dane do nowej bazy danych z lepszym schematem.

Moglibyśmy stworzyć klasę DatabaseMigrationTask, która będzie obsługiwać tę migrację:

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

Dla uproszczenia, jesteśmy reprezentujących dwie bazy danych za pomocą listy z String . Mówiąc najprościej, nasza migracja polega na umieszczeniu danych z pierwszej listy na drugiej.

Aby wykonać tę migrację w pożądanym momencie, będziemy musieli użyć przeciążony wersję harmonogramu () metoda :

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

Jak widać, zadanie migracji oraz datę wykonania podajemy do metody schedule () .

Następnie migracja jest wykonywana w czasie wskazanym przez twoSecondsLater :

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

Chociaż jesteśmy przed tym momentem, migracja nie następuje.

3. Zaplanuj powtarzalne zadanie

Teraz, gdy omówiliśmy, jak zaplanować pojedyncze wykonanie zadania, zobaczmy, jak radzić sobie z powtarzalnymi zadaniami.

Po raz kolejny klasa Timer oferuje wiele możliwości : Możemy ustawić powtarzanie tak, aby obserwować stałe opóźnienie lub stałą szybkość.

Stałe opóźnienie oznacza, że ​​wykonanie rozpocznie się po pewnym czasie od momentu, w którym rozpoczęło się ostatnie wykonanie, nawet jeśli było opóźnione (a zatem samo jest opóźnione) .

Powiedzmy, że chcemy zaplanować jakieś zadanie co dwie sekundy i że pierwsze wykonanie zajmuje jedną sekundę, a drugie dwie, ale jest opóźnione o jedną sekundę. Wtedy trzecie wykonanie rozpocznie się w piątej sekundzie:

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

Z drugiej strony, stała stopa oznacza, że ​​każda realizacja będzie zgodna z pierwotnym harmonogramem, bez względu na to, czy poprzednia realizacja została opóźniona .

Skorzystajmy ponownie z poprzedniego przykładu, ze stałą szybkością, drugie zadanie rozpocznie się po trzech sekundach (z powodu opóźnienia). Ale trzeci po czterech sekundach (przestrzegając początkowego harmonogramu jednego uruchomienia co dwie sekundy):

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

Omawiając te dwie zasady, zobaczmy, jak z nich korzystać.

Aby użyć planowania ze stałym opóźnieniem, istnieją jeszcze dwa przeciążenia metody schedule () , z których każde przyjmuje dodatkowy parametr określający okresowość w milisekundach.

Dlaczego dwa przeciążenia? Ponieważ nadal istnieje możliwość rozpoczęcia zadania w określonym momencie lub po pewnym opóźnieniu.

Jeśli chodzi o planowanie według stałej stawki, mamy dwie metody scheduleAtFixedRate () również przyjmujące okresowość w milisekundach. Ponownie mamy jedną metodę uruchamiania zadania w określonym dniu io określonej godzinie, a drugą - po określonym czasie.

Warto również wspomnieć, że jeśli zadanie zajmuje więcej czasu niż okres do wykonania, opóźnia cały łańcuch wykonań, niezależnie od tego, czy używamy stałego opóźnienia, czy stałej stawki.

3.1. Ze stałym opóźnieniem

Teraz wyobraźmy sobie, że chcemy wdrożyć system biuletynów, wysyłając e-mail do naszych obserwujących co tydzień. W takim przypadku powtarzalne zadanie wydaje się idealne.

Więc planujmy biuletyn co sekundę, co w zasadzie jest spamem, ale ponieważ wysyłanie jest fałszywe, możemy iść!

Najpierw zaprojektujmy biuletyn Zadanie :

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

Za każdym razem, gdy jest wykonywane, zadanie wydrukuje swój zaplanowany czas, który zbieramy za pomocą metody TimerTask # scheduleExecutionTime () .

A co by było, gdybyśmy chcieli zaplanować to zadanie co sekundę w trybie stałego opóźnienia? Będziemy musieli użyć przeciążonej wersji schedule () , o której mówiliśmy wcześniej:

new Timer().schedule(new NewsletterTask(), 0, 1000); for (int i = 0; i < 3; i++) { Thread.sleep(1000); }

Oczywiście przeprowadzamy testy tylko w kilku przypadkach:

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

Jak widać, między każdym wykonaniem jest co najmniej jedna sekunda, ale czasami są one opóźnione o milisekundę. Zjawisko to wynika z naszej decyzji o zastosowaniu powtarzania o stałym opóźnieniu.

3.2. Ze stałą stawką

A co by było, gdybyśmy użyli powtórzeń o stałej stawce? Wtedy musielibyśmy użyć metody 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

Ten samouczek zilustrował wiele sposobów wykorzystania prostej, ale elastycznej infrastruktury Timer i TimerTask wbudowanej w Javę do szybkiego planowania zadań. Oczywiście w świecie Javy istnieją znacznie bardziej złożone i kompletne rozwiązania - takie jak biblioteka Quartz - ale jest to bardzo dobre miejsce do rozpoczęcia.

Implementację tych przykładów można znaleźć w projekcie GitHub - jest to projekt oparty na Eclipse, więc powinien być łatwy do zaimportowania i uruchomienia w obecnej postaci.