• 6 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 30/11/2016

Les Synchronyzers

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Les Synchronyzers sont des mécanismes qui permettent de préserver une portion de code de tout accès concurrent afin d'en assurer l'intégrité.
En fait, nous avons déjà vu certains de ces objets dans un des chapitres précédents… Vous ne devinez pas ? il s'agit des objets de type Lock. :)

Le moment est venu de vous parler des autres mécanismes de ce genre, afin de clôturer ce cours sur la programmation concurrente.
Je vous propose donc de faire un tour de présentation de ces objets pour ensuite vous montrer un cas pratique, par objet.

Prêt ? Alors allons-y !

Tour d'horizon

Avant de rentrer directement dans la pratique, voici un listing des différents Synchronyzers que vous pouvez utiliser ainsi que leurs fonctions et leurs modes d'utilisation.

Type d'objet

Que fait-il ?

Quand l'utiliser ?

Semaphore

Permet à un certain nombre de threads d'être mis en attente jusqu'à ce qu'une autorisation de poursuivre soit envoyée.

Sert à limiter les accès concurrents à une ressource en fonction d'un certains nombre d'autorisations.
Un exemple : vous êtes dans un parc d'attraction et vous souhaitez faire un manège à sensation. Seulement voilà, toutes les places sont prises : celui-ci n'accepte que 50 places assises et tous les sièges sont pris. Il vous faudra donc attendre qu'une place se libère afin de pouvoir utiliser le manège.

SynchronousQueue

Cet objet permet à un thread de passer une ressource (un objet en fait) à un autre.

C'est le principe du thread producteur et du thread consommateur. L'un dépose des ressources et l'autre les consomme.

CyclicBarrier

Pose une barrière commune à plusieurs threads afin que ceux-ci patientent avant de continuer leurs traitements.

Par exemple, lorsque nous devons attendre la fin de traitement de X threads avant de traiter un résultat.
Une image assez simple serait la ligne de départ d'une course : les coureurs (les threads) sont en action mais ils attendent tous le signal de départ afin de continuer.

Exchanger

Celui-ci permet à deux threads de s'échanger des objets entre eux. À la différence de l'objet SynchronousQueue, l'échange se fait dans les deux sens ici.

Pour pouvoir communiquer entre deux threads, sans synchronisation explicite.

CountDownLatch

Il fonctionne un peu comme un compte à rebours pour les threads. Il permet à des threads de patienter jusqu'à ce qu'un compteur ai été ramené à 0.

Ressemble à l'objet CyclicBarrier mais diffère sur quelques aspects…

Tout ces objets ont un but et un contexte d'utilisation et, pour bien comprendre ceci, je vous propose quelques exemples.

Action, réaction !

Maintenant, passons aux choses sérieuses. Nous allons voir l'utilisation de ces différents Synchronizers via des exemples qui, je l'espère, seront assez parlants.

Semaphore

Comme je vous le disais précédemment, les sémaphores permettent de mettre un verrou en fonction d'un entier, celui-ci représentant un seuil d'acceptation d'utilisation d'une ressource (repensez aux places de libres sur le manège).
Cet objet a un constructeur prenant un entier comme paramètre : celui-ci sera la limite autorisée pour la ressource ; il possède aussi deux méthodes :

  • acquire() : opération de décrémentation atomique sur le compteur du sémaphore ;

  • release() : opération d'incrémentation atomique sur le compteur du sémaphore.

Ce sera donc via ces méthodes que nous allons réserver ou libérer une ressource.
Pour comprendre ceci, je vous propose un exemple simple : un restaurant et ses clients.

package Semaphore;

import java.util.Random;
import java.util.concurrent.Semaphore;

public class Client implements Runnable {

   String name;
   Semaphore sem;
   
   public Client(String pName, Semaphore pSem){
      name = pName;
      sem = pSem;
   }
   
   public void run() {
      try {
         //Nous prenons une réservation de ressource
         sem.acquire();
         
         Random rand = new Random();
         
         //Pour avoir une pause conséquente et bien
         //et bien voir le mécanisme du sémaphore.
         long pause = 0;
         while(pause < 8000)pause = rand.nextInt(15000);
         
         System.out.println(name + " : Je mange au restaurant pendant " + pause/1000 + " secondes");
         
         Thread.sleep(pause);
         
         System.err.println(name + " : Au revoir. Je quitte le restaurant. ");
         
         //Pour libérer la ressource
         sem.release();
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
}
package Semaphore;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class Restaurant {
   public static void main(String[] args) {
      //Bon, c'est un restaurant à 5 places...
      //C'est petit, mais en Bretagne, il y en a. ;)
      Semaphore sem = new Semaphore(5);
      
      ExecutorService execute = Executors.newCachedThreadPool();
      
      int i = 0;
      while(true){         
         Client cli = new Client("Client N°" + (++i), sem);
         execute.execute(cli);
         
         try {
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
}

Et le résultat de ce code nous donne :

Utilisation d'un objet Semaphore
Utilisation d'un objet Semaphore

Lors de l'exécution, vous avez pu voir que lorsque le nombre de clients à table atteint le nombre de 5, les autres ne peuvent pas entrer et utiliser les places du restaurant. Ce n'est que lorsqu'un client sort du restaurant et libère une place qu'un autre client peut y siéger : voilà le fonctionnement des sémaphores.

Mais pourquoi acquire() décrémente le compteur ? La limite n'est pas de 5 dans notre cas ?

Si, bien sûr, mais lorsque le compteur atteint 0, la ressource n'est plus disponible, il faut que le compteur soit supérieur à 0 pour pouvoir y accéder.

CyclicBarrier

Cet objet a un principe de fonctionnement assez simple : il pose une barrière, définie à l'avance par un entier. Cette barrière stoppe les threads qui y arrivent et les met en attente jusqu'à ce que le nombre de threads atteignant ladite barrière corresponde au nombre défini par nos soins. Une fois que le nombre de threads ayant atteint la barrière correspond à notre quota, tous les threads reprennent leurs tâches.

Voici un programme d'exemple :

package CyclicBarrier;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;

public class CBExemple implements Callable<Integer>{

   int start, end, resultat;
   CyclicBarrier barrier;
   String name;
   public CBExemple(int pStart, int pEnd, CyclicBarrier pBarrier, String pName){
      start = pStart;
      end = pEnd;
      barrier = pBarrier;
      name = pName;
   }
   
   
   public Integer call(){
      
      System.out.println("Le thread " + name + "  se met en action");
      
      //Nous allons faire en sorte que tous les threads
      //s'attendent lorsque ceux-ci atteignent la moitié de leurs
      //travail attitré
      
      int moitie = end - start / 2 ;
      resultat = 0;
      
      while(start < end){
         
         resultat += start;
         start++;
         
         try {
            Thread.sleep(1);
         } catch (InterruptedException e1) {
            e1.printStackTrace();
         }
         
         if(start == moitie){
            try {
               System.err.println("Le thread " + name + " a atteint la moitié de sa tâche");
               System.err.println("\t -> " + (barrier.getNumberWaiting() + 1) + " threads actuellement à la barrière !");
               
               //Cette invocation indique que le thread est arrivé à la barrière
               //Il attend maintenant que la limite soit atteinte
               //pour pouvoir franchir la barrière
               barrier.await();
               
               System.out.println("Barrière dépassée : Le thread " + name + " se remet à l'oeuvre !");
            } catch (InterruptedException | BrokenBarrierException e) {
               e.printStackTrace();
            }
         }
      }

      return resultat;
   }  
   
   public int getResultat(){
      return resultat;
   }
   
   public String getName(){
      return name;
   }
}
package CyclicBarrier;

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Barrier {

   public static void main(String[] args){
      
      
      ExecutorService execute = Executors.newFixedThreadPool(4);
      CyclicBarrier barrier = new CyclicBarrier(4);
      
      CBExemple cbe1, cbe2, cbe3, cbe4;
      cbe1 = new CBExemple(0, 100, barrier, "Thread-0-100");
      cbe2 = new CBExemple(1_000, 5_000, barrier, "Thread-1000-5000");
      cbe3 = new CBExemple(5_000, 15_000, barrier, "Thread-5000-15000");
      cbe4 = new CBExemple(10_000, 50_000, barrier, "Thread-10000-50000");
      
      Future<Integer> ft1 = execute.submit(cbe1);
      Future<Integer> ft2 = execute.submit(cbe2);
      Future<Integer> ft3 = execute.submit(cbe3);
      Future<Integer> ft4 = execute.submit(cbe4);
      
      try {
         System.out.println("Total = " + (ft1.get() + ft2.get() + ft3.get() + ft4.get()));
      } catch (InterruptedException |ExecutionException e) {
         e.printStackTrace();
      }
      
      execute.shutdown();
   }
}

Ce qui me donne :

Résultat d'utilisation de l'objet CyclicBarrier
Résultat d'utilisation de l'objet CyclicBarrier

C'est un peu comme sur un champ de course où tous les chevaux sont dans les starting-blocks. :)

Cet objet permet aussi de déclencher un Runnable lorsque tout le monde a atteint la barrière, juste avant que celle-ci ne cède sous le poids de nos threads. Afin de vous montrer ceci, j'ai légèrement modifié notre précédent code et j'ai rajouté une nouvelle classe qui va se charger de voir où en sont nos threads dans leurs traitements. :)

package CyclicBarrier.Runnable;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;

public class CBExemple implements Callable<Integer>{

   int start, end, resultat;
   CyclicBarrier barrier;
   String name;
      
   public CBExemple(int pStart, int pEnd, String pName){
      start = pStart;
      end = pEnd;      
      name = pName;
   }
   
   
   public Integer call(){
      
      System.out.println("Le thread " + name + "  se met en action");
      
      //Nous allons faire en sorte que tous les threads
      //s'attendent lorsque ceux-ci atteignent la moitié de leurs
      //travail attitré
      
      int moitie = end - start / 2 ;
      resultat = 0;
      
      while(start < end){
         
         resultat += start;
         start++;
         
         try {
            Thread.sleep(1);
         } catch (InterruptedException e1) {
            e1.printStackTrace();
         }
         
         if(start == moitie){
            try {
               System.err.println("Le thread " + name + " a atteint la moitié de sa tâche");
               System.err.println("\t -> " + (barrier.getNumberWaiting() + 1) + " threads actuellement à la barrière !");
               
               //Cette invocation indique que le thread est arrivé à la barrière
               //Il attend maintenant que la limite soit atteinte
               //pour pouvoir franchir la barrière
               barrier.await();
               
               System.out.println("Barrière dépassée : Le thread " + name + " se remet à l'oeuvre !");
            } catch (InterruptedException | BrokenBarrierException e) {
               e.printStackTrace();
            }
         }
      }

      return resultat;
   }  
   
   public int getResultat(){
      return resultat;
   }
   
   public String getName(){
      return name;
   }
   
   //nous affectons maintenant la barrière après coup
   public void setBarrier(CyclicBarrier pBarrier){
      barrier = pBarrier;
   }
}
package CyclicBarrier.Runnable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

public class AfterBarrier implements Runnable {

   List<Callable<Integer>> listCallable = new ArrayList<>();
   
   public AfterBarrier(List<Callable<Integer>> pList){
      listCallable = pList;
   }
   
   public void run() {
      
      //Une petite pause pour bien voir dans la console
      try {
         Thread.currentThread().sleep(1000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      
      System.out.println("------------------------------------------------------");
      System.out.println("La barrière vient d'être atteinte par tout le monde ! ");
      System.out.println("Voilà où ils en sont : ");
      
      //On parcours notre liste d'objets
      for(Callable<Integer> call : listCallable){
         //On cast et on affiche le résultat actuel
         CBExemple cbe = (CBExemple)call;
         System.out.println("\t -> " + cbe.getName() + " : " + cbe.getResultat());         
      }
      
      System.out.println("------------------------------------------------------");
      
      try {
         Thread.currentThread().sleep(2000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }      
   }
}
package CyclicBarrier.Runnable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Barrier {

   public static void main(String[] args){

      CBExemple cbe1, cbe2, cbe3, cbe4;
      cbe1 = new CBExemple(0, 100, "Thread-0-100");
      cbe2 = new CBExemple(1_000, 5_000, "Thread-1000-5000");
      cbe3 = new CBExemple(5_000, 15_000, "Thread-5000-15000");
      cbe4 = new CBExemple(10_000, 50_000, "Thread-10000-50000");
      
      //Nous allons utiliser une liste pour lancer tous nos threads
      ArrayList<Callable<Integer>> tasks = new ArrayList<>();
      tasks.add(cbe1);
      tasks.add(cbe2);
      tasks.add(cbe3);
      tasks.add(cbe4);
      
      ExecutorService execute = Executors.newFixedThreadPool(4);
      
      //Cet objet accepte un deuxième argument qui est un Runnable
      //permettant de faire une action lorsque la barrière cède
      CyclicBarrier barrier = new CyclicBarrier(4, new AfterBarrier(tasks));
      
      //Nous mettons maintenant notre barrière dans nos objets Callable<Integer>
      cbe1.setBarrier(barrier);
      cbe2.setBarrier(barrier);
      cbe3.setBarrier(barrier);
      cbe4.setBarrier(barrier);
      
      try {
         //Cette méthode est nouvelle pour vous
         //Vous pouvez ainsi lancer une lister de threads
         //Et récupérer une liste d'objet Future<T> : un par objet Callable<T>
         List<Future<Integer>> listFuture = execute.invokeAll(tasks);
         
         int resultat = 0;
         
         //On parcourt les résultats
         for(Future<Integer> ft : listFuture)
            resultat += ft.get();
         
         System.out.println("Total : " + resultat);
         
      } catch (InterruptedException |ExecutionException e) {
         e.printStackTrace();
      }
      execute.shutdown();
   }
}

Ce qui me donne :

Résultat d'utilisation de l'objet CyclicBarrier
Résultat d'utilisation de l'objet CyclicBarrier

Simple et pratique à la fois. Vous pouvez donc utiliser ce type de verrou lorsque vos threads doivent s'attendre mutuellement avant de poursuivre leurs actions. Par exemple, avec cette méthode vous pourrez lancer un jeu multi-joueurs seulement lorsque tous les joueurs sont prêts.

CountDownLatch

Cet objet fonctionne un peu de la même manière que l'objet précédent à quelques différences près… Déjà celui-ci ne peut pas être réutilisé !
Il y a aussi une autre différence de taille, cette objet s'apparente plus à un compte à rebours qu'à une barrière. Plusieurs threads peuvent être bloqués en attendant que le compte à rebours atteigne zéro et débloque la situation.

Cet objet s'initialise comme l'objet précédent, avec un entier dans son constructeur. Celui-ci représente le nombre de départs du compte à rebours.
Certains threads seront mis en attente directement via la méthode await(). Ces threads seront libérés de leur état d'attente uniquement lorsque le compteur atteindra zéro, via des appels successifs à la méthode countDown().

C'est à vous de définir le compte à rebours et ce qui décrémente ce fameux compte. En guise d'exemple, je vous propose un code assez simple :

package CountDownLock;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.concurrent.CountDownLatch;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class Fenetre extends JFrame{

   JButton bouton = new JButton("Décrémenter le compte à rebours");   
   JLabel info = new JLabel();

   CountDownLatch lock;
   
   public Fenetre(CountDownLatch pLock){

     lock = pLock;
     
     JPanel panneau = new JPanel();
     panneau.setLayout(new BorderLayout());
     
     info.setText("Compte à rebours : " + lock.getCount());
     info.setHorizontalAlignment(JLabel.CENTER);
     
     panneau.add(bouton, BorderLayout.NORTH);
     panneau.add(info, BorderLayout.SOUTH);
     getContentPane().add(panneau);
     
     setTitle("CountDownLatch");
     setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
     setLocationRelativeTo(null);
     pack();
     
     
     bouton.addActionListener(new ActionListener(){
           public void actionPerformed(ActionEvent e){
              lock.countDown();
              info.setText("Compte à rebours : " + lock.getCount());
           }
     });
     
     setVisible(true);
   }
}
package CountDownLock;

import java.util.concurrent.CountDownLatch;

public class Test {

   static CountDownLatch lock = new CountDownLatch(10);
   
   public static void main(String[] args) {

      Thread t1 = new Thread(new Runnable(){
         public void run(){
            System.out.println("Premier thread en attente !..");
            try {
               lock.await();
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            System.out.println("Premier thread libéré  après le compte à rebours");
         }
      });

      Thread t2 = new Thread(new Runnable(){
         public void run(){
            System.out.println("Deuxième thread en attente !..");
            try {
               lock.await();
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            System.out.println("Deuxième thread libéré  après le compte à rebours");
         }
      });
      
      Fenetre fen = new Fenetre(lock);
      t1.start();
      t2.start();
   }
}

Ce qui me donne, après exécution :

Avant que le compte à rebours ne soit à zéro…
Avant que le compte à rebours ne soit à zéro…
Après que le compte à rebours soit à zéro…
Après que le compte à rebours soit à zéro…

Ici nous avons utilisé un événement utilisateur pour décrémenter le compteur mais nous aurions pu utiliser un thread annexe, comme ceci :

package CountDownLock;

import java.util.concurrent.CountDownLatch;

public class Test2 {

   static CountDownLatch lock = new CountDownLatch(10);
   
   public static void main(String[] args) {

      Thread t1 = new Thread(new Runnable(){
         public void run(){
            System.out.println("Premier thread en attente !..");
            try {
               lock.await();
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            System.out.println("Premier thread libéré  après le compte à rebours");
         }
      });

      Thread t2 = new Thread(new Runnable(){
         public void run(){
            System.out.println("Deuxième thread en attente !..");
            try {
               lock.await();
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            System.out.println("Deuxième thread libéré  après le compte à rebours");
         }
      });
      
     
      t1.start();
      t2.start();
      
      Thread t3 = new Thread(new Runnable(){
         public void run(){
            while(true){
               try {
                  Thread.sleep(1000);
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }
               
               lock.countDown();
               System.out.println(lock.getCount() + "...");
               if(lock.getCount() == 0){
                  System.out.println("Top départ pour les threads bloqués !");
                  break;
               }
            }
         }
      });
      t3.start();
   }
}

Résultat :

Après que le compte à rebours soit à zéro…
Après que le compte à rebours soit à zéro…

SynchronousQueue

Exchanger

Cet objet permet à deux threads de s'échanger un objet entre eux. Par exemple, nous pourrions avoir un thread qui initialise une liste de documents à déplacer et un autre thread qui se charge de les déplacer. Une fois tous les déplacement faits, la liste des documents à traiter est vide et demande une nouvelle liste au thread qui détermine quel sont les documents à traiter : ils s'échangent donc leurs listes respectives et ainsi de suite…

Voici un exemple pour mieux comprendre :

package Exchanger;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Exchanger;

public class FileFinder implements Runnable {
   private List<String> listDocument = new ArrayList<>();
   private List<String> listDocumentInitial = new ArrayList<>();
   Exchanger exchanger;
   
   public FileFinder(Exchanger ex){
      exchanger = ex;
      listDocumentInitial.add("fichier 1");
      listDocumentInitial.add("fichier 2");
      listDocumentInitial.add("fichier 3");
      listDocumentInitial.add("fichier 4");
      listDocumentInitial.add("fichier 5");
   }
   
   public void run() {
      int numEchange = 1;
      while(true){
         
         System.out.println("---------------------------------------");
         System.out.println("Contenu de la liste côté trouveur : ");
         System.out.println(listDocument);
         System.out.println("---------------------------------------");
         Iterator<String> it = listDocumentInitial.iterator();
         
         while(it.hasNext()){
            //On traite avec notre objet
            String nom = numEchange + "-" + it.next();
            listDocument.add(nom);
            System.out.println("[+] Ajout de " + nom + " dans la collection");
            try {
               Thread.sleep(2500);
            } catch (InterruptedException e) {
               e.printStackTrace();         
            }
         }         
         
         //Lorsque la liste est vide, on demande à récupérer une liste pleine
         try {
            System.err.println("\t -> Liste remplie du côte du trouveur de fichier !");
            listDocument = (List<String>)exchanger.exchange(listDocument);
            numEchange++;
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }  
   }
}
package Exchanger;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Exchanger;

public class FileMover implements Runnable {
   private List<String> listDocument = new ArrayList<>();
   Exchanger exchanger;
   
   public FileMover(Exchanger ex){
      exchanger = ex;
   }
   
   public void run() {

      while(true){
       
         System.out.println("---------------------------------------");
         System.out.println("Contenu de la liste côté déplaceur : ");
         System.out.println(listDocument);
         System.out.println("---------------------------------------");
         Iterator<String> it = listDocument.iterator();
         
         while(it.hasNext()){
            String nom = it.next();
            
            //On traite avec notre objet
            it.remove();
            System.out.println("[-] Suppression de " + nom + " dans la collection");
            try {
               Thread.sleep(1500);
            } catch (InterruptedException e) {
               e.printStackTrace();         
            }
         }
         
         
         //Lorsque la liste est vide, on demande à récupérer une liste pleine
         try {
            System.err.println("\t -> Liste vide du côte du déplaceur de fichier !");
            listDocument = (List<String>)exchanger.exchange(listDocument);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }               
   }
}
package Exchanger;

import java.util.concurrent.Exchanger;

public class Test {

   public static void main(String[] args) {
      
      Exchanger ex = new Exchanger();
      Thread t1 = new Thread(new FileFinder(ex));
      Thread t2 = new Thread(new FileMover(ex));

      t1.start();
      t2.start();
      
   }
}

Et le résultat me donne quelque chose comme ceci !

Utilisation d'un Exchanger
Utilisation d'un Exchanger

Que ce passe-t-il concrètement ? C'est simple. Nous avons deux threads qui sont initialisés et qui travaillent avec une liste de document, ici une simple liste de chaînes de caractères pour simplifier la chose. Ces deux threads ont deux rôles différents :

  1. l'un remplit une liste avec des chaînes de caractères ;

  2. l'autre vide petit à petit sa liste.

Ces deux threads ont, à un moment, terminé leur travail et doivent fournir le fruit de leurs labeurs à leur congénère.
Le premier thread tente de passer sa liste de mots au second lorsque celle-ci atteint une certaine taille et le second thread réclame une liste de mots lorsque sa liste est vide. C'est au moment où les deux conditions sont remplies que l'échange d'objet s'effectue, pas avant. Tant que les deux threads n'invoquent pas ensemble la méthode exchange(), l'un attend l'autre.

Simple et pratique à la fois. :magicien:

Avouez que ce chapitre était moins difficile, non ?!
Vous connaissez maintenant les bases importantes de la programmation concurrente. Mais si vous souhaitez en apprendre d'avantage, je vous invite à faire quelques recherche sur Internet, vous y trouverez de nombreuses ressources sur le sujet.

Voilà, ce cours touche à sa fin…
J'espère que tout ce que nous avons vu vous a permis de bien comprendre les tenants et aboutissants de la programmation en environnement multithread et que vous êtes maintenant autonomes sur le sujet.

Comme vous devez aussi vous en douter, la programmation concurrente est un domaine très complexe et il pourrait faire l'objet d'un ouvrage à lui tout seul.
D'ailleurs, il existe déjà un tel ouvrage sur le sujet, si vous voulez en découvrir plus sur cet aspect de la programmation, voici un excellent ouvrage que je peux vous conseiller.

Et si vous souhaitez passer à un autre versant de Java, n'hésitez pas à consulter d'autres de mes cours d'approfondissement : 

  • Java et le XML pour utiliser des fichiers XML avec Java ;

  • Java et les annotations pour ajouter des informations sémantiques à votre code ;

  • Java et les collections, pour traiter efficacement vos données avec des piles, des files, des maps etc. ;

  • Et mon cours qui va sortir prochainement, Java et la programmation réseau, pour faire dialoguer vos programmes en Java avec d'autres machines.

Exemple de certificat de réussite
Exemple de certificat de réussite