Temporizador de Java

1. Temporizador: conceptos básicos

Timer y TimerTask son clases de utilidades de Java que se utilizan para programar tareas en un hilo en segundo plano. En pocas palabras, TimerTask es la tarea a realizar y Timer es el programador .

2. Programe una tarea una vez

2.1. Después de un retraso determinado

Comencemos simplemente ejecutando una sola tarea con la ayuda de un temporizador :

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

Ahora, esto realiza la tarea después de un cierto retraso , dado como segundo parámetro del método schedule () . Veremos en la siguiente sección cómo programar una tarea en una fecha y hora determinadas.

Tenga en cuenta que si estamos ejecutando esta es una prueba de JUnit, debemos agregar una llamada Thread.sleep (delay * 2) para permitir que el hilo del temporizador ejecute la tarea antes de que la prueba de Junit deje de ejecutarse.

2.2. En una fecha y hora determinadas

Ahora, veamos el método Timer # schedule (TimerTask, Date) , que toma una fecha en lugar de un largo como segundo parámetro, lo que nos permite programar la tarea en un determinado instante, en lugar de después de un retraso.

Esta vez, imaginemos que tenemos una antigua base de datos heredada y queremos migrar sus datos a una nueva base de datos con un esquema mejor.

Podríamos crear una clase DatabaseMigrationTask que manejará esa migración:

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

Para simplificar, representamos las dos bases de datos mediante una lista de cadenas . En pocas palabras, nuestra migración consiste en colocar los datos de la primera lista en la segunda.

Para realizar esta migración en el instante deseado, tendremos que usar la versión sobrecargada del método schedule () :

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

Como podemos ver, le damos la tarea de migración así como la fecha de ejecución al método schedule () .

Luego, la migración se ejecuta en el momento indicado por twoSecondsLater :

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

Mientras estamos antes de este momento, la migración no ocurre.

3. Programe una tarea repetible

Ahora que hemos cubierto cómo programar la ejecución única de una tarea, veamos cómo lidiar con las tareas repetibles.

Una vez más, hay múltiples posibilidades que ofrece la clase Timer : Podemos configurar la repetición para observar un retraso fijo o una tasa fija.

Un retraso fijo significa que la ejecución comenzará un período de tiempo después del momento en que comenzó la última ejecución, incluso si se retrasó (por lo tanto, se retrasó) .

Digamos que queremos programar alguna tarea cada dos segundos, y que la primera ejecución toma un segundo y la segunda toma dos, pero se retrasa un segundo. Entonces, la tercera ejecución comenzaría en el quinto segundo:

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

Por otro lado, una tasa fija significa que cada ejecución respetará el cronograma inicial, sin importar si se ha retrasado una ejecución anterior .

Reutilicemos nuestro ejemplo anterior, con una tasa fija, la segunda tarea comenzará después de tres segundos (debido al retraso). Pero, el tercero a los cuatro segundos (respetando el cronograma inicial de una ejecución cada dos segundos):

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

Cubiertos estos dos principios, veamos cómo usarlos.

Para utilizar la programación de retardo fijo, hay dos sobrecargas más del método schedule () , cada una de las cuales toma un parámetro adicional que indica la periodicidad en milisegundos.

¿Por qué dos sobrecargas? Porque todavía existe la posibilidad de iniciar la tarea en un momento determinado o después de un cierto retraso.

En cuanto a la programación de tasa fija, tenemos los dos métodos scheduleAtFixedRate () que también toman una periodicidad en milisegundos. Nuevamente, tenemos un método para iniciar la tarea en una fecha y hora determinadas y otro para iniciarla después de un retraso determinado.

También vale la pena mencionar que, si una tarea toma más tiempo que el período para ejecutarse, retrasa toda la cadena de ejecuciones, ya sea que usemos retardo fijo o tasa fija.

3.1. Con retraso fijo

Ahora, imaginemos que queremos implementar un sistema de newsletter, enviando un correo electrónico a nuestros seguidores cada semana. En ese caso, una tarea repetitiva parece ideal.

Entonces, programemos el boletín cada segundo, que básicamente es spam, pero como el envío es falso, ¡estamos listos para comenzar!

Primero diseñemos un boletín de noticias .

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

Cada vez que se ejecuta, la tarea imprimirá su hora programada, que recopilamos usando el método TimerTask # scheduleExecutionTime () .

Entonces, ¿qué pasa si queremos programar esta tarea cada segundo en modo de retardo fijo? Tendremos que usar la versión sobrecargada de schedule () de la que hablamos anteriormente:

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

Por supuesto, solo llevamos a cabo las pruebas para algunas ocurrencias:

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

Como podemos ver, hay al menos un segundo entre cada ejecución, pero a veces se retrasan un milisegundo. Ese fenómeno se debe a nuestra decisión de utilizar la repetición de retardo fijo.

3.2. Con tarifa fija

Ahora, ¿qué pasaría si usáramos una repetición de tasa fija? Entonces tendríamos que usar el método 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

Este tutorial ilustró las muchas formas en que puede hacer uso de la infraestructura Timer y TimerTask, simple pero flexible , integrada en Java, para programar tareas rápidamente. Por supuesto, existen soluciones mucho más complejas y completas en el mundo de Java si las necesita, como la biblioteca Quartz, pero este es un muy buen lugar para comenzar.

La implementación de estos ejemplos se puede encontrar en el proyecto GitHub; este es un proyecto basado en Eclipse, por lo que debería ser fácil de importar y ejecutar tal como está.