Comment démarrer un fil de discussion en Java

Comment démarrer un fil de discussion en Java

1. introduction

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

This is very useful, in particular when dealing with long or recurring operations that can’t run on the main thread, ou lorsque l'interaction avec 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 lesLife Cycle of a Thread in Java.

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

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

Essayons un exemple basique, en étendant la classeThread:

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 seconde classe pour initialiser et démarrer notre thread:

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

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 ressemble beaucoup aux exemples que nous pouvons trouver en ligne.

Bien sûr,this is far from production-ready code, where it’s of critical importance to manage resources in the correct way, to avoid too much context switching or too much memory usage.

So, to get production-ready we now need to write additional boilerplate à traiter:

  • la création cohérente de nouveaux threads

  • le nombre de threads en direct simultanés

  • désallocation des threads: très important pour les threads de démon afin d'éviter les fuites

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

3. Le cadre deExecutorService

LeExecutorService implémente le modèle de conception du pool de threads (également appelé modèle de travail répliqué ou modèle d'équipage de travail) et prend en charge la gestion des threads que nous avons mentionnée ci-dessus, en plus d'ajouter des fonctionnalités très utiles comme la réutilisabilité des threads et les files d'attente de tâches.

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 de gestion de la mémoire importante.

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 quelques 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 lesExecutorService,, veuillez lire nosGuide to the Java ExecutorService.

4. Commencer 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();
});

Nous pouvons utiliser deux méthodes:execute, qui ne renvoie rien, etsubmit, qui renvoie unFuture encapsulant le résultat du calcul.

Pour plus d'informations surFutures,, veuillez lire nosGuide to java.util.concurrent.Future.

5. Démarrer une tâche avecCompletableFutures

Pour récupérer le résultat final d'un objetFuture, nous pouvons utiliser la méthodeget 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 devions augmenter la complexité de notre code.

Java 1.8 a introduit un nouveau framework en plus de la constructionFuture pour mieux travailler avec le résultat du calcul: lesCompletableFuture.

CompletableFuture implémenteCompletableStage, 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.

L'implémentation pour soumettre une tâche est beaucoup plus simple:

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

supplyAsync prend unSupplier 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 auxForkJoinPool.commonPool(), ou nous pouvons spécifier lesExecutor que nous préférons comme second paramètre.

Pour en savoir plus surCompletableFuture,, veuillez lire nosGuide To CompletableFuture.

6. Exécution de tâches retardé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 pouvant nous aider à exécuter des opérations retardées ou récurrentes:

  • java.util.Timer

  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Timer

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 programme 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 écoulée.

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

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor a des méthodes similaires à la classeTimer:

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


Pour terminer notre exemple, nous utilisonsscheduleAtFixedRate() pour les tâches récurrentes:

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


Le code ci-dessus exécutera une tâche après un délai initial de 100 millisecondes, et après cela, il exécutera la même tâche toutes les 450 millisecondes.

Si le processeur ne peut pas terminer le traitement de la tâche à temps avant l'occurrence suivante, lesScheduledExecutorService attendront que la tâche en cours soit terminée, avant de commencer la suivante.

Pour éviter ce temps d'attente, nous pouvons utiliserscheduleWithFixedDelay(), qui, comme décrit par son nom, garantit un délai de longueur fixe entre les itérations de la tâche.

Pour plus de détails surScheduledExecutorService,, veuillez lire nosGuide to the 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,how do we choose the right tool?

Lorsqu'un framework 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.

Timer:

  • n'offre pas de garanties en temps réel: il planifie les tâches en utilisant la méthodeObject.wait(long) 

  • il n'y a qu'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

  • les exceptions d'exécution lancées dans unTimerTask tueraient le seul thread disponible, tuant ainsiTimer

ScheduledThreadPoolExecutor:

  • peut être configuré avec n'importe quel nombre de threads

  • peut tirer parti de tous les cœurs de processeur disponibles

  • attrape les exceptions d'exécution et nous permet de les gérer si nous le voulons (en remplaçant la méthodeafterExecute deThreadPoolExecutor)

  • annule la tâche qui a lancé l'exception, tout en laissant les autres continuer à s'exécuter

  • s'appuie sur le système de planification du système d'exploitation pour garder une trace des fuseaux horaires, des retards, de l'heure solaire, etc.

  • fournit une API collaborative si nous avons besoin de coordination entre plusieurs tâches, comme attendre la fin de toutes les tâches soumises

  • fournit une meilleure API pour la gestion du cycle de vie des threads

Le choix est maintenant évident, non?

7. Différence entreFuture etScheduledFuture

Dans nos exemples de code, nous pouvons observer queScheduledThreadPoolExecutor renvoie un type spécifique deFuture:ScheduledFuture.

ScheduledFuture étend les interfacesFuture etDelayed, héritant ainsi de la méthode supplémentairegetDelay qui renvoie le délai restant associé à la tâche en cours. Il est étendu deRunnableScheduledFuture qui ajoute une méthode pour vérifier si la tâche est périodique.

ScheduledThreadPoolExecutor implémente toutes ces constructions via la classe interneScheduledFutureTask 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 cadres disponibles pour démarrer des threads et exécuter des tâches en parallèle.

Ensuite, nous avons approfondi les différences entreTimer etScheduledThreadPoolExecutor.

Le code source de l'article est disponibleover on GitHub.