IFT-2030

Démonstration 10

23 mars 1999


Java - Threads et Sockets


INTRODUCTION AUX THREADS

    Les threads en Java sont des objets représentant des programmes.  L'avantage d'utiliser les threads est qu'il permet l'exécution en parallèle de plusieurs programmes, tout en permettant l'utilisation de ressources communes tels des emplacements mémoire.  L'objet thread possède une procédure membre run, qui sera exécutée lors du démarrage du thread.  Voici un résumé de la classe Thread:
    class Thread extends Object implements Runnable
      {
        // Constructeurs (il en existe plusieurs)
        public Thread();
        // ...

        // Constantes
        public static final int MAX_PRIORITY;
        public static final int MIN_PRIORITY;
        public static final int NORM_PRIORITY;

        // Sous-ensemble de méthodes de classe
        public synchronized void start() throws IllegalArgumentException;
        public void run();
        public final void stop();
        public final void suspend();
        public final void resume();
        public static native void sleep(long millis) throws InterruptedException;
        public static void sleep(long millis,int nanos) throws InterruptedException;
        public final void setPriority(int newPriority);
        public static native void yield();
      }

    L'interface Runnable se définit comme suit:
    public interface Runnable
      {
        public void run();
      }

    L'exception InterruptedException signale qu'un thread a été interrompu.



EXEMPLE SIMPLE DE THREAD

    On peut donc définir un thread en définissant une sous-classe de Thread.  Exemple:
    class HelloWorld extends Thread
      {
        int delai;
        String msgnom;
        public HelloWorld(int d,String n)
          {delai=d;msgnom="Je m'appelle "+n;}
        public void run()
          {
            System.out.println(msgnom+" et je dormirai au moins "+delai+" sec.");
            try{sleep(1000*delai);}
            catch(InterruptedException e) {};
            System.out.println(msgnom+" et je vous quitte.");
          }
      }
    Voici un programme qui utilise la classe de thread défini:
    public class test
      {
        public static void main(String[] args)
          {
            HelloWorld[] T=new HelloWorld[5];
            T[0]=new HelloWorld(2,"ALPHA");
            T[1]=new HelloWorld(3,"BETA");
            T[2]=new HelloWorld(5,"GAMMA");
            T[3]=new HelloWorld(1,"DELTA");
            T[4]=new HelloWorld(4,"EPSILON");
            for(int i=0;i<5;i++) T[i].start();
          }
      }
    Le programme imprime à l'écran:
Je m'appelle ALPHA et je dormirai au moins 2 sec.
Je m'appelle BETA et je dormirai au moins 3 sec.
Je m'appelle GAMMA et je dormirai au moins 5 sec.
Je m'appelle DELTA et je dormirai au moins 1 sec.
Je m'appelle EPSILON et je dormirai au moins 4 sec.
Je m'appelle DELTA et je vous quitte.
Je m'appelle ALPHA et je vous quitte.
Je m'appelle BETA et je vous quitte.
Je m'appelle EPSILON et je vous quitte.
Je m'appelle GAMMA et je vous quitte.

EXCLUSION MUTUELLE AVEC SYNCHRONISATION

    On peut garantir l'exclusivité d'une ressource en utilisant le mot-clé synchronized sur des méthodes ou des blocs d'instructions.

   Syntaxe:   synchronized (<expression>) <code>
<expression> est un objet ou un tableau et <code> est une section critique. Le code de la section critique n'est exécuté que lorsqu'un "lock" sur la ressource <expression> est obtenu. Le "lock" est relâché après la sortie de la section critique.

    static void abs(int[] valeurs)
      {
        System.out.println("Je commence.");
        synchronized(valeurs)
          {
            for(int i=0;i<valeurs.length;i++)
              if(valeurs[i]<0)
                valeurs[i]=-valeurs[i];
          }
        System.out.println("J'ai terminé.");
      }

    Le mot-clé synchronized est aussi utilisé pour les méthodes. Dans ce cas, la méthode entière est considérée comme étant une section critique.

    Exemple de synchronisation par méthode:
    class Compte
      {
        int solde;
        public synchronized int getsolde() {return solde;}
        public synchronized void depot(int m) {solde+=m;}
      }
    Ici les méthodes getsolde et depot verrouillent l'objet.  Il est donc impossible que 2 threads qui appellent respectivement getsolde et depot puissent être exécutés simulatément (ou avec intercalage).

   Autre exemple:
    class Test {
      static int a = 1;
      static int b = 2;
      public static void modifier()
      {
        a = 3;
        b = 4;
      }
      public static void afficher()
      {
        System.out.println("a= " + a + ", b= " + b);
      }
    }

    Si on a 2 threads qui appellent respectivement modifier() et afficher(), le thread qui appelle afficher() pourrait afficher:
    a= 1, b= 2
    a= 1, b= 4
    a= 3, b= 2
    a= 3, b= 4
    Ceci est dû au fait que la méthode afficher() peut lire la valeur de a avant ou après sa modification dans modifier() et elle peut lire la valeur de b avant ou après sa modification dans modifier().

    Un résultat similaire est obtenu avec les 2 modifications suivantes:

    class SyncTest1 {
      static int a = 1;
      static int b = 2;
      public static synchronized void modifier()
      {
        a = 3;
        b = 4;
      }
      public static void afficher()
      {
        System.out.println("a= " + a + ", b= " + b);
      }
    }

    class SyncTest2 {
      static int a = 1;
      static int b = 2;
      public static void modifier()
      {
        a = 3;
        b = 4;
      }
      public static synchronized void afficher()
      {
        System.out.println("a= " + a + ", b= " + b);
      }
    }

    Si on veut que le thread qui appelle afficher() imprime soit "a= 1, b= 2", soit "a= 3, b= 4",il faut que les 2 méthodes modifier() et afficher() soient déclarées synchronized. Dans ce cas, le thread qui appelle modifier doit obtenir le "lock" sur la classe avant que le code de la méthode ne soit exécuté et le thread qui appelle afficher doit aussi obtenir le "lock" sur la classe avant que le code de la méthode ne soit exécuté. Or étant donné que chaque classe ou objet a UN SEUL lock qui lui est associé, l'exécution de modifier et afficher ne pourra pas être intercalée.

    class SyncSyncTest {
      static int a = 1;
      static int b = 2;
      public static synchronized void modifier()
      {
        a = 3;
        b = 4;
      }
      public static synchronized void afficher()
      {
        System.out.println("a= " + a + ", b= " + b);
      }
    }


WAIT ET NOTIFY

Les fonctions wait et notify sont 2 fonctions de la classe Object. Elles sont déclarées ainsi:
    public final native void wait(long timeout) throws InterruptedException;
    public final void wait(long timeout, int nanos) throws InterruptedException;
    public final void wait() throws InterruptedException;
    public final native void notify();
    public final native void notifyAll();

    Un appel à la fonction wait à partir d'un thread T qui a verrouillé un objet met le thread dans un état "dormant" et "dé-verrouille" (unlock) l'objet. Le thread demeure alors inactif (ou "dormant") jusqu'à ce qu'un des 3 évènements suivants se produise:

    Le thread T doit alors verrouillé de nouveau l'objet avant de poursuivre son exécution (d'où la possibilité d'une compétition entre plusieurs threads pour essayer d'obtenir le "lock").

    Note: les méthodes wait, notify et notifyAll ne peuvent être invoquées que si le thread a verrouillé l'objet.

    Exemple: Supposons qu'on a 2 threads représentant respectivement un client et un manufacturier. Le manufacturier peut déposer de la marchandise dans un dépôt alors que le client peut retirer de la marchandise du dépôt.
    Un système représentant ce problème doit tenir compte de 2 contraintes importantes:

Solution utilisée:


      public class Depot
      {
        private Marchandise marchandise;
        private boolean marchandise_disponible = false;

        public synchronized Marchandise retirer() {
          while (marchandise_disponible == false) {
            try {
              wait();
            } catch (InterruptedException e) { }
          }
          marchandise_disponible = false;
          notifyAll();
          return marchandise;
        }

        public synchronized void deposer(Marchandise nouvelle_marchandise) {
          while (marchandise_disponible == true) {
            try {
              wait();
            } catch (InterruptedException e) { }
          }
          try {
            marchandise = nouvelle_marchandise.clone();
          } catch (CloneNotSupportedException e) { }
          marchandise_disponible = true;
          notifyAll();
        }
      }

    Note: A un moment donné, il est possible d'avoir plusieurs threads clients et manufacturiers. Les threads clients vont être inactifs ("dormants") tant que le dépôt est vide. Quand ce dernier est rempli par un manufacturier, tous les threads "dormants" vont être avertis et chacun d'entre eux (ainsi que les threads manufacturiers) va essayer d'obtenir le lock sur l'objet pour retirer la marchandise. Un seul thread client pourra réussir. Les autres devront attendre que le dépôt soit rempli de nouveau. La compétition existe aussi entre les manufacturiers: les threads manufacturiers vont être inactifs tant que le dépôt est vide. Quand un thread client retire la marchandise, tous les manufacturiers (et tous les clients) seront avertis. A un moment donné, un seul manufacturier peut obtenir le lock sur l'objet pour remplir le dépôt. Tous les autres threads manufacturiers sont obligés de demeurer inactifs en attendant que le dépôt soit vidé par un client.


ORDONANCEMENT DES THREADS

    Il est possible d'associer des priorités aux threads, via la fonction membre setPriority.  Il peut donc être utile de donner une priorité plus faible à des threads, lorsque l'on ne veut pas que le système s'engorge.  Par exemple, lorsqu'une machine est un serveur FTP, on ne voudrait pas que les connections de l'extérieur viennent surcharger tout le système.  En plaçant ces threads en priorité basse, on peut s'assurer que les threads de priorité normale ne seront pas trop ralentis. On pourra utiliser les constantes MAX_PRIORITY, MIN_PRIORITY et NORM_PRIORITY pour choisir nos priorités.

CESSION DU CONTRÔLE

    Les applications multithread fonctionnent en passant le contrôle aux threads tour à tour pour un certain temps(quelques millisecondes habituellement). Il est possible toutefois de relâcher le contrôle avant que le système ne le fasse, ce qui permet de donner du temps aux autres threads.  On utilisera la fonction membre yield() pour ce faire.  Voici un exemple:
    class Dire extends Thread
      {
        String word;
        public Dire(String s) {word=s;}
        public void run()
          {
            for(int i=0;i<5;i++)
              {
                System.out.println(word);
                yield();
              }
          }
        public static void main(String[] args)
          {
            new Dire("allo").start();
            new Dire("bonjour").start();
          }
      }
    Ce programme donne comme sortie:
allo
bonjour
allo
bonjour
allo
bonjour
allo
bonjour
allo
bonjour
    Sans le yield, le programme donnerait fort probablement:
allo
allo
allo
allo
allo
bonjour
bonjour
bonjour
bonjour
bonjour

SOCKETS

    Les Sockets représentent une méthode de communication entre un programme client et un programme serveur dans un réseau.


    Pseudocode pour décrire le comportement d'un serveur:

    Choisir un port qui sera utilisé pour établir une connection
    while (true) {
      Attendre une demande de connexion d'un client
      Etablir une connexion avec le client
      Traiter les requêtes du client (thread)
    }

    Pseudocode pour décrire le traitement des requêtes:

    À la création:
        Obtenir un InputStream et un OutputStream pour
         communiquer avec le client
         Exécution:
       Tant que le client envoie des requêtes {
            Lire une requête sur l'InputStream
            Répondre sur l'OutputStream
       }

    Pseudocode pour décrire le comportement du client:

    Ouvrir une connexion à une adresse et un port donné
    Obtenir un InputStream et un OutputStream pour
     communiquer avec le serveur
    Envoyer des requêtes sur l'OutputStream
    Lire les réponses sur l'InputStream

    Note: plusieurs connexions peuvent être établies avec le serveur.

    Afin de faciliter les communications réseau, l'objet Socket est très utile en Java, particulièrement lorsque vient le temps de se connecter sur une adresse internet.  Voici quelques détails sur les sockets en Java:
    *** Exemple de client sur page WEB du cours ***

    Pour recevoir des connections, on se sert plutôt de l'objet ServerSocket.  Voici des détails sur les ServerSocket en Java:
    *** Exemple de serveur sur page WEB du cours ***


HASHTABLE

    Voici un résumé de la classe Hashtable:

    public class Hashtable extends Dictionary implements Cloneable, Serializable
    {
      public Hashtable();    // il existe d'autres constructeurs
      public synchronized void clear();
      public synchronized boolean contains(Object value);
      public synchronized boolean containsKey(Object key);
      public synchronized Object get(Object key);
      public synchronized Object put(Object key, Object value);
      // ...
    }

    Et voici un exemple d'utilisation de la classe Hashtable. Un agenda est créé, qui contient une Hashtable qui associe une activité à une date. La date y est utilisée comme clé.

    import java.util.*;

    class Agenda
    {
      Hashtable h;

      agenda()
      { h = new Hashtable(); }

      public void set(Date d, String s)
      { h.put(d,s); }

      public String get(Date d)
      {
        if (d == null)
        {
          Date now = new Date();
          d = new Data(now.getYear(), now.getMonth(), now.getDay());
        }
        return (String) h.get(d);
      }
    }

    public class Cal
    {
      public static void main(String[] argv)
      {
        Agenda A = new Agenda();
        Date now = new Date();
        Date d = new Data(now.getYear(), now.getMonth(), now.getDay());
        A.set(d, "Demo 2030");
        System.out.println(A.get(d));
      }
    }

QUESTIONS ET COMMENTAIRES

dift2030@jsp.umontreal.ca
Retour à la page principale