Comment démarrer un thread en Java

1. Introduction

Dans ce didacticiel, nous allons explorer différentes façons de démarrer un thread et d'exécuter des tâches parallèles.

Ceci est très utile, en particulier lorsqu'il s'agit d'opérations longues ou récurrentes qui ne peuvent pas s'exécuter sur le thread principal , ou lorsque l'interaction de l'interface utilisateur ne peut pas être mise en attente en attendant les résultats de l'opération.

Pour en savoir plus sur les détails des threads, lisez certainement notre tutoriel sur le cycle de vie d'un thread en Java.

2. Les bases de l'exécution d'un thread

Nous pouvons facilement écrire une logique qui s'exécute dans un thread parallèle en utilisant le framework Thread .

Essayons un exemple basique, en étendant la classe Thread :

public class NewThread extends Thread { public void run() { long startTime = System.currentTimeMillis(); int i = 0; while (true) { System.out.println(this.getName() + ": New Thread is running..." + i++); try { //Wait for one sec so it doesn't print too fast Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ... } } }

Et maintenant, nous écrivons une deuxième classe pour initialiser et démarrer notre thread:

public class SingleThreadExample { public static void main(String[] args) { NewThread t = new NewThread(); t.start(); } }

Nous devrions appeler la méthode start () sur les threads dans l' état NEW (l'équivalent de non démarré). Sinon, Java lèvera une instance de l' exception IllegalThreadStateException .

Supposons maintenant que nous devions démarrer plusieurs threads:

public class MultipleThreadsExample { public static void main(String[] args) { NewThread t1 = new NewThread(); t1.setName("MyThread-1"); NewThread t2 = new NewThread(); t2.setName("MyThread-2"); t1.start(); t2.start(); } }

Notre code a toujours l'air assez simple et très similaire aux exemples que nous pouvons trouver en ligne.

Bien sûr, c'est loin d'être un code prêt pour la production, où il est d'une importance critique de gérer les ressources de la bonne manière, pour éviter trop de changement de contexte ou une utilisation excessive de la mémoire.

Donc, pour être prêt pour la production, nous devons maintenant écrire un passe-partout supplémentaire pour traiter:

  • la création cohérente de nouveaux threads
  • le nombre de threads en direct simultanés
  • la désallocation des threads: très importante pour les threads démons afin d'éviter les fuites

Si nous le voulons, nous pouvons écrire notre propre code pour tous ces scénarios et même d'autres, mais pourquoi devrions-nous réinventer la roue?

3. Le cadre ExecutorService

Les ExecutorService met en œuvre le pool de threads modèle de conception (également appelé un modèle de travailleur ou équipage répliquées) et prend en charge la gestion des threads nous l' avons mentionné plus haut, plus il ajoute quelques fonctionnalités très utiles comme un fil et des files d' attente réutilisabilité tâche.

La réutilisabilité des threads, en particulier, est très importante: dans une application à grande échelle, l'allocation et la désallocation de nombreux objets thread crée une surcharge importante de gestion de la mémoire.

Avec les threads de travail, nous minimisons la surcharge causée par la création de threads.

Pour faciliter la configuration du pool, ExecutorService est livré avec un constructeur simple et des options de personnalisation, telles que le type de file d'attente, le nombre minimum et maximum de threads et leur convention de dénomination.

Pour plus de détails sur ExecutorService, veuillez lire notre Guide de Java ExecutorService.

4. Démarrage d'une tâche avec des exécuteurs

Grâce à ce cadre puissant, nous pouvons changer notre état d'esprit du démarrage des threads à la soumission de tâches.

Voyons comment nous pouvons soumettre une tâche asynchrone à notre exécuteur:

ExecutorService executor = Executors.newFixedThreadPool(10); ... executor.submit(() -> { new Task(); });

Il existe deux méthodes que nous pouvons utiliser: execute , qui ne renvoie rien, et submit , qui renvoie un Future encapsulant le résultat du calcul.

Pour plus d'informations sur Futures, veuillez lire notre Guide de java.util.concurrent.Future.

5. Démarrer une tâche avec CompletableFutures

Pour récupérer le résultat final d'un objet Future , nous pouvons utiliser la méthode get disponible dans l'objet, mais cela bloquerait le thread parent jusqu'à la fin du calcul.

Alternativement, nous pourrions éviter le blocage en ajoutant plus de logique à notre tâche, mais nous devons augmenter la complexité de notre code.

Java 1.8 a introduit un nouveau framework au-dessus de la construction Future pour mieux travailler avec le résultat du calcul: le CompletableFuture .

CompletableFuture implémente CompletableStage , qui ajoute une vaste sélection de méthodes pour attacher des rappels et éviter toute la plomberie nécessaire pour exécuter des opérations sur le résultat une fois qu'il est prêt.

La mise en œuvre pour soumettre une tâche est beaucoup plus simple:

CompletableFuture.supplyAsync(() -> "Hello");

supplyAsync prend un Supplier contenant le code que nous voulons exécuter de manière asynchrone - dans notre cas, le paramètre lambda.

La tâche est maintenant soumise implicitement à ForkJoinPool.commonPool () , ou nous pouvons spécifier l' exécuteur que nous préférons comme deuxième paramètre.

Pour en savoir plus sur CompletableFuture, veuillez lire notre Guide de CompletableFuture.

6. Exécution de tâches différées ou périodiques

Lorsque vous travaillez avec des applications Web complexes, nous pouvons avoir besoin d'exécuter des tâches à des moments spécifiques, peut-être régulièrement.

Java dispose de peu d'outils qui peuvent nous aider à exécuter des opérations différées ou récurrentes:

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Minuteur

Timer est une fonction pour planifier des tâches pour une exécution future dans un thread d'arrière-plan.

Les tâches peuvent être planifiées pour une exécution unique ou pour une exécution répétée à intervalles réguliers.

Voyons à quoi ressemble le code si nous voulons exécuter une tâche après une seconde de retard:

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

Ajoutons maintenant un calendrier récurrent:

timer.scheduleAtFixedRate(repeatedTask, delay, period);

Cette fois, la tâche s'exécutera après le délai spécifié et elle sera récurrente après la période de temps écoulée.

Pour plus d'informations, veuillez lire notre guide sur Java Timer.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor has methods similar to the Timer class:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); ScheduledFuture resultFuture = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

To end our example, we use scheduleAtFixedRate() for recurring tasks:

ScheduledFuture resultFuture = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

The code above will execute a task after an initial delay of 100 milliseconds, and after that, it'll execute the same task every 450 milliseconds.

If the processor can't finish processing the task in time before the next occurrence, the ScheduledExecutorService will wait until the current task is completed, before starting the next.

To avoid this waiting time, we can use scheduleWithFixedDelay(), which, as described by its name, guarantees a fixed length delay between iterations of the task.

Pour plus de détails sur ScheduledExecutorService, veuillez lire notre Guide de Java ExecutorService.

6.3. Quel outil est le meilleur?

Si nous exécutons les exemples ci-dessus, le résultat du calcul est le même.

Alors, comment choisir le bon outil ?

Lorsqu'un cadre offre plusieurs choix, il est important de comprendre la technologie sous-jacente pour prendre une décision éclairée.

Essayons de plonger un peu plus profondément sous le capot.

Minuterie :

  • n'offre pas de garanties en temps réel: tâches il planifie l' aide de la Object.wait (long) méthode
  • il y a un seul thread d'arrière-plan, donc les tâches s'exécutent séquentiellement et une tâche de longue durée peut en retarder d'autres
  • runtime exceptions thrown in a TimerTask would kill the only thread available, thus killing Timer

ScheduledThreadPoolExecutor:

  • can be configured with any number of threads
  • can take advantage of all available CPU cores
  • catches runtime exceptions and lets us handle them if we want to (by overriding afterExecute method from ThreadPoolExecutor)
  • cancels the task that threw the exception, while letting others continue to run
  • relies on the OS scheduling system to keep track of time zones, delays, solar time, etc.
  • provides collaborative API if we need coordination between multiple tasks, like waiting for the completion of all tasks submitted
  • provides better API for management of the thread life cycle

The choice now is obvious, right?

7. Difference Between Future and ScheduledFuture

In our code examples, we can observe that ScheduledThreadPoolExecutor returns a specific type of Future: ScheduledFuture.

ScheduledFuture extends both Future and Delayed interfaces, thus inheriting the additional method getDelay that returns the remaining delay associated with the current task. It's extended by RunnableScheduledFuture that adds a method to check if the task is periodic.

ScheduledThreadPoolExecutor implémente toutes ces constructions via la classe interne ScheduledFutureTask et les utilise pour contrôler le cycle de vie de la tâche.

8. Conclusions

Dans ce tutoriel, nous avons expérimenté les différents frameworks disponibles pour démarrer des threads et exécuter des tâches en parallèle.

Ensuite, nous avons approfondi les différences entre Timer et ScheduledThreadPoolExecutor.

Le code source de l'article est disponible à l'adresse over sur GitHub.