Temporizator Java

1. Cronometru - elementele de bază

Timer și TimerTask sunt clase java util utilizate pentru a programa sarcini într-un fir de fundal. În câteva cuvinte - TimerTask este sarcina de efectuat, iar Timer este programatorul .

2. Programează o sarcină o dată

2.1. După o întârziere dată

Să începem pur și simplu executând o singură sarcină cu ajutorul unui temporizator :

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

Acum, aceasta efectuează sarcina după o anumită întârziere , dată ca al doilea parametru al metodei schedul () . Vom vedea în secțiunea următoare cum să planificați o sarcină la o dată și oră date.

Rețineți că, dacă rulăm, acesta este un test JUnit, ar trebui să adăugăm un apel Thread.sleep (întârziere * 2) pentru a permite firului Timerului să ruleze sarcina înainte ca testul Junit să se oprească din executare.

2.2. La o dată și o dată date

Acum, să vedem metoda Timer # schedule (TimerTask, Date) , care ia o dată în loc de o lungime ca al doilea parametru, permițându-ne să programăm sarcina într-un anumit moment, mai degrabă decât după o întârziere.

De data aceasta, să ne imaginăm că avem o bază de date veche și vrem să le migram datele într-o nouă bază de date cu o schemă mai bună.

Am putea crea o clasă DatabaseMigrationTask care va gestiona migrarea:

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

Pentru simplitate, reprezentăm cele două baze de date printr-o Listă de șiruri . Pur și simplu, migrarea noastră constă în plasarea datelor din prima listă în a doua.

Pentru a efectua această migrare în momentul dorit, va trebui să folosim versiunea supraîncărcată a metodei schedul () :

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

După cum putem vedea, oferim sarcina de migrare, precum și data executării, metodei schedul () .

Apoi, migrarea se execută la ora indicată de twoSecondsLater :

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

În timp ce ne aflăm înainte de acest moment, migrația nu are loc.

3. Programați o activitate repetabilă

Acum că am analizat cum să planificăm executarea unică a unei sarcini, să vedem cum să ne ocupăm de sarcini repetabile.

Încă o dată, există multiple posibilități oferite de clasa Timer : Putem seta repetarea pentru a observa fie o întârziere fixă, fie o rată fixă.

O întârziere fixă ​​înseamnă că execuția va începe o perioadă de timp după momentul în care a început ultima execuție, chiar dacă a fost întârziată (deci fiind ea însăși întârziată) .

Să presupunem că vrem să programăm unele sarcini la fiecare două secunde și că prima execuție durează o secundă și a doua durează două, dar este întârziată cu o secundă. Apoi, a treia execuție ar începe la a cincea secundă:

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

Pe de altă parte, o rată fixă ​​înseamnă că fiecare execuție va respecta programul inițial, indiferent dacă o execuție anterioară a fost întârziată .

Să refolosim exemplul anterior, cu o rată fixă, a doua sarcină va începe după trei secunde (din cauza întârzierii). Dar, al treilea după patru secunde (respectând programul inițial al unei execuții la fiecare două secunde):

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

Aceste două principii fiind acoperite, să vedem cum să le folosim.

Pentru a utiliza programarea cu întârziere fixă, există încă două suprasolicitări ale metodei schedul () , fiecare luând un parametru suplimentar care indică periodicitatea în milisecunde.

De ce două suprasarcini? Deoarece există încă posibilitatea de a începe sarcina într-un anumit moment sau după o anumită întârziere.

În ceea ce privește programarea cu rată fixă, avem cele două metode schedulAtFixedRate () luând, de asemenea, o periodicitate în milisecunde. Din nou, avem o metodă pentru a începe sarcina la o dată și oră date și alta pentru a o începe după o anumită întârziere.

De asemenea, merită menționat faptul că, dacă o sarcină durează mai mult decât perioada de executare, aceasta întârzie întregul lanț de execuții, indiferent dacă folosim întârziere fixă ​​sau rată fixă.

3.1. Cu o întârziere fixă

Acum, să ne imaginăm că vrem să implementăm un sistem de newsletter, trimitând un e-mail adepților noștri în fiecare săptămână. În acest caz, o sarcină repetitivă pare ideală.

Deci, haideți să programăm buletinul informativ în fiecare secundă, care este practic spam, dar, deoarece trimiterea este falsă, suntem bine să plecăm!

Să proiectăm mai întâi un NewsletterTask :

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

De fiecare dată când se execută, sarcina își va imprima ora programată, pe care o adunăm folosind metoda TimerTask # schedulExecutionTime () .

Atunci, dacă vrem să programăm această sarcină în fiecare secundă în modul cu întârziere fixă? Va trebui să folosim versiunea supraîncărcată a calendarului () despre care am vorbit mai devreme:

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

Desigur, efectuăm testele doar pentru câteva apariții:

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

După cum putem vedea, există cel puțin o secundă între fiecare execuție, dar uneori sunt întârziate cu o milisecundă. Acest fenomen se datorează deciziei noastre privind repetarea folosită cu întârziere fixă.

3.2. Cu o rată fixă

Acum, dacă ar fi să folosim o repetare cu rată fixă? Apoi ar trebui să folosim metoda schedulAtFixedRate () :

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

Acest tutorial a ilustrat numeroasele moduri în care puteți utiliza infrastructura simplă, dar flexibilă, Timer și TimerTask încorporată în Java, pentru planificarea rapidă a sarcinilor. Există, desigur, soluții mult mai complexe și complete în lumea Java, dacă aveți nevoie de ele - cum ar fi biblioteca Quartz -, dar acesta este un loc foarte bun pentru a începe.

Implementarea acestor exemple poate fi găsită în proiectul GitHub - acesta este un proiect bazat pe Eclipse, deci ar trebui să fie ușor de importat și rulat așa cum este.