begin process at 2012 02 15 11:51:19
  Trouver un code source :
 
dans
 
Accueil > 

Tutoriels

 > 

Api

 > JAVA ET LA SYNCHRONISATION

JAVA ET LA SYNCHRONISATION


 Information sur le tutoriel

Déposé par rom1v le 26/10/2006 17:43:41
Dans la catégorie Api
Vu : 29 716 fois
 

Ecrire un message privé à l'auteur
Commentaire sur cette source (16)
Ajouter un commentaire et/ou une note

Note :
10 / 10 - par 3 personnes
10,00 / 10

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

 Description

La synchronisation est un élément essentiel dès lors que vous utilisez plusieurs threads (c'est-à-dire dans quasiment toutes les applications). En effet, sans synchronisation, il est impossible de développer une application robuste qui fonctionne quel que soit l'entrelacement de l'exécution des threads.

Tutorial

Java et la synchronisation

Introduction

La synchronisation est un élément essentiel dès lors que vous utilisez plusieurs threads (c'est-à-dire dans quasiment toutes les applications). En effet, sans synchronisation, il est impossible de développer une application robuste qui fonctionne quel que soit l'entrelacement de l'exécution des threads.

Pré requis : Connaître la syntaxe Java, la programmation orientée objet et le fonctionnement général des threads et de l'ordonnanceur.

Présentation des problèmes généraux

Tout d'abord, voyons un problème de synchronisation classique. Plusieurs entités récepteurs (threads) reçoivent une demande d'un client. Ils peuvent déposer des travaux dans une file. Les ouvriers prennent les travaux déposés, les traitent, et fournissent un résultat dans une autre file. L'émetteur récupère ces résultats et peut les envoyer au client.

problème

Plusieurs problèmes de synchronisation se posent ici :

  1. Plusieurs récepteurs ne peuvent pas déposer simultanéement un travail dans la file des travaux, sinon les travaux seraient sur la même case ;
  2. Si la file des travaux est pleine, les récepteurs doivent attendre qu'une case se libère pour déposer un nouveau travail ;
  3. Plusieurs ouvriers ne peuvent pas prendre chacun un travail simultanéement, sinon ils auraient le même travail à traiter ;
  4. Si la file des travaux est vide, les ouvriers doivent attendre qu'un travail soit déposé pour pouvoir le traiter ;
  5. Plusieurs ouvriers ne peuvent pas déposer simultanéement le résultat d'un travail dans la file des résultats, sinon les résultats seraient sur la même case ;
  6. Si la file des résultats est pleine, les ouvriers doivent attendre qu'une case se libère pour déposer un nouveau résultat ;
  7. Si la file des résultats est vide, l'émetteur doit attendre qu'un résultat soit disponible.

Objectifs

Le but est de résoudre ces problèmes généraux, auquels on peut dans la majorité des cas se ramener.

Pour les résoudre correctement, il faut assurer :

  • La sûreté : rien de mauvais ne se produit, quelque soit l'entrelacement des threads (deux ouvriers ne peuvent jamais prendre le même travail) ;
  • La vivacité : quelque chose de bon finit par se produire (si la file des travaux n'est pas vide, un ouvrier finira par prendre les travaux).

Il faut aussi prendre en compte les cas possibles ; par exemple, deux travaux déposés consécutivement peuvent être exécutés séquentiellement par le même ouvrier.

Exclusion mutuelle de sections critiques

L'exclusion mutuelle permet de résoudre les problèmes de synchronisations 1, 3 et 5 de la présentation des problèmes généraux.

Voici un exemple de programme qui ne respecte pas la sûreté, si plusieurs threads peuvent y accéder simultanéement :

class  ListeTab  {

     private  String[] tab =  new  String[ 50 ];
     private  int  index =  0 ;

     void  ajoute ( String s ) {
         tab [ index = s;
         index++;
     }

}

Ce programme, comme vous l'aurez deviné, permet de gérer une liste de Strings en utilisant un tableau.

void  ajoute ( String s ) {
     tab[index] = s;  //(a1)
     index++;        //(a2)
}
void  ajoute ( String s ) {
     tab[index] = s;  //(b1)
     index++;        //(b2)
}

Soient deux threads T1 et T2 qui exécutent en parallèle (ou en pseudo-parallèle sur un mono-processeur) la fonction ajoute(String) sur la même ListeTab. Leurs actions peuvent être entrelacées, ainsi, plusieurs exécutions sont possibles. Par exemple :

  • (a1) (a2) (b1) (b2), est une exécution possible, cohérente ;
  • (b1) (b2) (a1) (a2), est une exécution possible, cohérente ;
  • (a1) (b1) (b2) (a2), est une exécution possible, mais incohérente : le tableau ne contient pas la chaîne de caractères ajoutée par T1, et une case de la liste est vide.

Plusieurs exécutions différentes peuvent conduire à des résultats différents, dont certains sont incohérents. Ce problème récurrent peut être résolu par l'exclusion mutuelle (mutex).

La fonction ajoute est appelée section critique. Plusieurs sections critiques dépendantes ne doivent jamais exécuter leur code simultanéement (par plusieurs threads différents) : on dit qu'elles sont en exclusion mutuelle. Dans l'exemple précédent, ajoute était en exclusion mutuelle avec elle-même, mais on peut bien sûr imaginer qu'elle sera aussi en exclusion mutuelle avec la fonction supprime...

Pour mettre en place l'exclusion mutuelle, il faut utiliser des verrous. Lorsqu'un thread entre dans une section critique, il demande le verrou. S'il l'obtient, il peut alors exécuter le code. S'il ne l'obtient pas, parce qu'un autre thread l'a déjà pris, il est alors bloqué en attendant de l'obtenir. Il est possible d'utiliser un nombre potentiellement infini de verrous, et donc faire des exclusions mutuelles précises : par exemple, a() doit être en exclusion mutuelle avec lui-même et avec b(), tandis que c() doit être en exclusion mutuelle avec lui-même et avec d(), mais pas avec a() et b()...

Une section critique est vue comme une opération atomique(une seule opération indivisible) par une autre section critique utilisant le même verrou.

Mot-clé synchronized

Pour réaliser ceci en Java, la méthode la plus simple est d'utiliser le mot-clé synchronized.

Voici comment il s'utilise :

synchronized ( unObjet ) {
     //section critique
}

unObjet représente un verrou, qui peut être un objet Java quelconque. Attention cependant, il vaut mieux utiliser des références déclarées final, pour être sûr que la référence vers l'objet n'est pas modifiée ; en effet, le cas de figure ci-dessous ne fonctionne pas, car lorsque plusieurs threads arrivent au bloc synchronisé, la variable maListe ne référence pas toujours le même objet, et donc ne représente pas toujours le même verrou.

synchronized (maListe) {
     maListe =  new  ArrayList<String>() ;
}

Cas particulier : Lorsque l'objet qui sert de verrou pour la synchronisation est this, et qu'il englobe tout le code d'une méthode, on peut mettre le mot-clé synchronized dans la signature de la méthode. Les deux codes ci-dessous sont strictement équivalents :

void  methode () {
     synchronized ( this ) {
         //section critique
     }
}
synchronized  void  methode () {
     //section critique
}

Package java.util.concurrent.locks

Une autre méthode pour réaliser des verrous est apparue dans Java 1.5. À première vue, elle peut paraître plus compliquée, mais elle est aussi plus puissante, dans le sens où elle permet de faire des choses que le mot-clé synchronized ne permet pas (nous y reviendrons plus tard).

Voici comment l'utiliser :

Lock l =  new  ReentrantLock () ;
l.lock () ;
try  {
     //section critique
finally  {
     l.unlock () ;
}

Pour comparer cette méthode avec la précédente, l.lock() et l.unlock() correspondent respectivement au début et à la fin du bloc synchronized.

Cependant, si vous désirez faire une exclusion mutuelle aussi simple que celles des exemples présentés ici, je vous conseille d'utiliser synchronized, et de réserver cette méthode pour des cas bien particuliers, que nous verrons ultérieurement.

Synchronisation coopérative

La synchronisation coopérative permet de résoudre les problèmes de synchronisations 2, 4, 6 et 7 de la présentation des problèmes généraux.

Info : Les deux premières solutions, présentées dans des exemples simples, utilisent plus ou moins le design pattern Moniteur.

Méthodes de la classe Object

Utilisation

La manière la plus basique de synchronisation entre plusieurs threads Java est l'utilisation des méthodes wait() et notify() (et éventuellement notifyAll()) définies dans la classe Object. Pour comprendre leur fonctionnement, complétons notre exemple de tout à l'heure :

class  ListeTab  {

     private  String []  tab =  new  String [ 50 ] ;
     private  int  index =  0 ;

     synchronized void  ajoute(String s) {
         tab [ index = s;
         index++;
         notify();
         System.out.println("notify() exécuté");
     }

     synchronized  String getPremierElementBloquant () {
         //tant que la liste est vide
         while ( index ==  0 ) {
             try  {
                 //attente passive
                 wait();
          &n catch ( InterruptedException ie ) {
                 ie.printStackTrace () ;
             }
         }
         return  tab [ 0 ] ;
     }
    
}

La méthode getPremierElementBloquant retourne le premier élément de la liste ; si la liste est vide, et bien elle attend qu'il y ait un élément ajouté.

Vous avez peut-être été surpris par la boucle while de la méthode getPremierElementBloquant plutôt qu'un simple if. Nous allons voir en détail ce qu'il se passe.

Supposons qu'un thread T1 exécute ajoute et qu'un thread T2 exécute getPremierElementBloquant.

Supposons que T2 ait d'abord la main, il prend le verrou(la méthode est définie synchronized), il trouve index == 0 vrai, donc il exécute wait(). Ce wait() est bloquant tant qu'un notify() sur le même objet ne le libère pas.

Remarque : Si plusieurs threads exécutent unObjet.wait(), chaque unObjet.notify() débloquera un thread bloqué, dans un ordre indéterminé.

Maintenant, T1 prend la main. il exécute ajoute, et donc demande le verrou. Mais vous allez me dire, il ne l'obtiendra pas, car c'est T2 qui a le verrou ! Et bien si, il l'obtiendra, car T2 a lâché le verrou dès lors qu'il a appelé la méthode wait().

Remarque : L'appel à la méthode wait() libère le verrou uniquement parce que l'objet sur lequel a été appliqué wait() (ici, this) est le même que le verrou.

T1 peut donc exécuter le code de la méthode ajoute. Lorsqu'il exécute notify(), il débloque T2 (de manière asynchrone). Mais, maintenant, bien sûr, T2n'a pas le verrou, puisque c'est T1 qui l'a (pour exécuter le System.out.println(...)). T2 est donc bloqué en attente du verrou, et d'autres threads (imaginons T3, T4...) peuvent l'obtenir avant lui. Supposons que T3 supprime tous les éléments de la liste (par une éventuelle méthode vider()), et qu'ensuite T2 prenne la main. T2 sort alors du wait(). Il a donc besoin de revérifier la conditionindex == 0, sinon il essaierait de récupérer l'élément à l'index 0 du tableau, qui est vide. C'est pour cela qu'il faut utiliser la boucle while.

Remarque : Il ne faut surtout pas confondre cette boucle while avec une attente active qui vérifirait en permanence que l'index est différent de 0. Ici, nous regardons si l'index est 0, si c'est le cas, nous mettons le thread en attente passive, qui sera réveillé uniquement lorsqu'un élément aura été ajouté. Une fois qu'il est réveillé, pour la raison que nous venons de voir, nous avons besoin de revérifier la condition.

Cette manière de procéder est pratique pour des problèmes de synchronisation simples ; pour des problèmes plus complexes, il vaut mieux utiliser d'autres méthodes. En l'occurence, la liste bloquante faisait objet d'exemple est totalement codée dans l'API Java 1.5, nous y reviendrons.

Limites

Hormis la difficulté d'implantation pour des problèmes complexes, cette solution atteint ses limites dès lors qu'une section critique doit être synchronisée sur plusieurs verrous.

class  UneClasse  {

     private final  Object lock1 =  new  Object () ;
     private final  Object lock2 =  new  Object () ;

     void  methode () {
          synchronized ( lock1 ) {
             synchronized ( lock2 ) {
                 while ( condition ) {
        &nbA?&??? ?? lock2.wait () ;
                 }
             }
         }
     }
    
     void  notifie () {
         lock2.notify () ;
     }
    
}

En effet, pour appliquer le wait(), nous sommes obligés de choisir l'un des deux verrous, et donc un seul des deux est libéré pendant l'attente. En l'occurence, ici, l'appel à methode() verrouille totalement lock1 tant que lock2.wait() ne s'est pas terminé.

Package java.util.concurrent.locks

La solution que nous avons vu pour réaliser l'exclusion mutuelle utilisant ce package permet aussi d'aller au-delà des limites des méthodes de la classe Object. Le principe est de n'utiliser qu'un seul verrou, mais avec plusieurs variables condition (sur lesquelles nous pourrons effectuer les opérations similaires à wait() et à notify()).

class  UneClasse  {

     private final  Lock lock =  new  ReentrantLock () ;
     private final  Condition cond1 = lock.newCondition () ;
     private final  Condition cond2 = lock.newCondition () ;

     void  methode1 ()  throws  InterruptedException  {
         lock.lock () ;
         try  {
             cond2.await () ;
             cond1.signal () ;
         finally  {
             lock.unlock () ;
         }
     }

     void  methode2 ()  throws  InterruptedException  {
         lock.lock () ;
         try  {
             cond1.await () ;
             cond2.signal () ;
         finally  {
             lock.unlock () ;
         }
     }
    
}

Maintenant, dans methode1() par exemple, l'appel à cond2.await() libère bien le verrou lock.

Une interface ReadWriteLock est aussi disponible dans ce package, permettant de résoudre les problèmes de lecture-écriture (à tout moment, soit un nombre quelconque de lecteurs, soit un et un seul écrivain). Je vous laisse consulter la documentation pour son utilisation, mais il est bon de savoir que cela existe.

Threads

Jusqu'ici, le mot thread désignait un processus léger. Dans cette section, le mot Thread signifiera la classe Thread de Java. La relation existant entre les deux est que l'appel à la méthode start() de la classe Thread crée un nouveau thread dans lequel du code est exécuté.

Ceci n'est pas un article sur la classe Thread, mais voici quelques méthodes à connaître :

  • static Thread currentThread() : Permet d'obtenir le thread courant.
  • static void yield() : Laisse une chance aux autres threads de s'exécuter.
  • static void sleep(long ms) throws InterruptedException : Suspend l'exécution du thread appelant pendant la durée indiquée (ne pas utiliser ceci à des fins de synchronisation !).
  • void interrupt() : provoque soit la levée de l'exception InterruptedException si l'activité est bloquée sur une opération de synchronisation, soit le positionnement d'un indicateur interrupted.
  • void join() : Attente bloquante de la terminaison de l'exécution du thread (jusqu'à ce que la méthode run() associée au Thread ait fini de s'exécuter).

Sémaphores

Impossible d'écrire un tutoriel concernant la synchronisation sans parler des sémaphores. Dans une présentation plus théorique des problèmes de synchronisation, les sémaphores seraient présentés avant les deux solutions précédentes ; mais, en pratique, vous n'aurez probablement pas besoin de les utiliser en Java. Cependant, je trouve que la compréhension de leur fonctionnement est importante.

La classe Semaphore est apparue dans Java 1.5.

Définition

Un sémaphore encapsule un entier, avec une contrainte de positivité, et deux opérations atomiques d'incrémentation et de décrémentation.

  • variable entière (toujours positive ou nulle).
  • opération P (acquire()) : décrémente le compteur s'il est strictement positif ; bloque s'il est nul en attendant de pouvoir le décrémenter.
  • opération V (release()) : incrémente le compteur.

On peut voir le sémaphore comme un ensemble de jetons, avec deux opérations :

  • Prendre un jeton, en attendant si nécessaire qu'il y en ait ;
  • Déposer un jeton.
Remarque : Les jetons déposés ne sont pas forcément ceux qui ont été pris.

Utilisation

Voyons un exemple très particulier de sémaphore à 1 jeton :

Semaphore sem =  new  Semaphore ( 1 ) ;
try  {
     sem.acquire () ;
     //section critique
     sem.release () ;
catch ( InterruptedException e ) {
     e.printStackTrace () ;
}

Un sémaphore à 1 jeton est très similaire à un verrou. Cependant, il ne faut pas les confondre ! En effet, si l'on effectue plusieurs fois l'opération V, un sémaphore garde en mémoire ces demandes en incrémentant l'entier qu'il utilise ; pour un verrou, effectuer plusieurs déverrouillages est strictement équivalent à n'effectuer qu'un seul déverrouillage. On peut ainsi voir plusieurs opérations V successives sur un sémaphore comme des notify() "retardés" sur un verrou.

Solutions implantées dans l'API 1.5

La version 1.5 de Java et son package java.util.concurrent (ainsi que ses sous-packages) fournissent des outils de synchronisation de plus haut niveau.

Par exemple, l'exemple que j'ai utilisé sensé simuler une liste bloquante est implanté par BlockingQueue (liste bloquante non bornée) et ArrayBlockingQueue (liste bloquante à tampon borné).

Un autre outil très utile est Executor. Il s'agit d'une liste d'attente bloquante d'actions à effectuer. On retrouve exactement le même mécanisme lorsque l'on utilise le thread dédié à l'affichage graphique (EventDispatchThread) de Swing : SwingUtilities.invokeLater(Runnable) met en file d'attente une action à effectuer dans le thread dédié à l'affichage graphique.

Voici un exemple d'utilisation d'Executor :

Executor executor = Executors.newSingleThreadExecutor () ;
executor.execute ( new  Runnable () {
     public  void  run () {
         System.out.println ( "appel asynchrone 1" ) ;
     }
}) ;
executor.execute ( new  Runnable () {
     public  void  run () {
         System.out.println ( "appel asynchrone 2" ) ;
     }
}) ;

Cet exemple exécute les Runnables de manière asynchrone vis-à-vis du thread courant, mais assure qu'ils seront appelés dans l'ordre.

D'autres outils sont également disponibles, consultez la documentation.

Inter-blocage

Le problème le plus courant en synchronisation lors du développement est l'inter-blocage. Un inter-blocage (ou deadlock) est un blocage qui intervient lorsque par exemple un thread A attend un thread B en même temps que B attend A.

Voici un exemple très simple d'inter-blocage :

class  DeadLock  {

     private final  Object lock1 =  new  Object () ;
     private final  Object lock2 =  new  Object () ;

     void  a ()  throws  InterruptedException  {
         lock1.wait () ;
         lock2.notify () ;
     }

     void  b ()  throws  InterruptedException  {
         lock2.wait () ;
         lock1.notify () ;
     }

}

Si un thread T1 exécute a() et qu'un thread T2 exécute b(), il y a inter-blocage. En effet, T1 attend le notify() de T2, mais pour que T2 appelle le notify(), il faut d'abord que T1 exécute son notify()...

Il faut absolument veiller à ne jamais avoir d'inter-blocage.

Éviter la synchronisation inutile

La synchronisation est quelque chose d'essentiel dans beaucoup de situations. Cependant, elle coûte cher en ressources processeur : il ne faut pas tout synchroniser. Si une méthode ne peut être appelée que d'un seul et unique thread, ne la synchronisez pas. En l'occurence, les méthodes d'écouteurs Swing seront forcément appelés dans l'EDT, donc inutile de synchroniser quoi que ce soit (sauf si vous créez à partir de là de nouveaux threads).

Lorsque vous utilisez beaucoup de synchronisation, essayez de voir si le modèle d'une file d'attente unique ne convient pas mieux (Executor), cela simplifiera de plus beaucoup votre code...

Évitez les anciennes Collections datant de Java 1.0 (Vector, Hashtable...) qui sont par défaut synchronisées, mais utilisez les nouvelles datant de Java 1.2 (ArrayList, HashMap...) qui ne sont pas synchronisées (meilleures performances). Pour récupérer une vue synchronisée d'une Collection, il suffit de faire Collections.synchronizedCollection(Collection).


Commentaires

Commentaire de Twinuts le 13/11/2006 10:01:57 administrateur CS

Salut,

c'est sympas, clair, concis et bien présenté.

10/10

bonne continuation ++

Commentaire de iiironhead le 05/06/2007 21:57:45

un grand merci !!!

Commentaire de sheorogath le 18/08/2007 13:34:55 administrateur CS

tres bon tuto !!!

10/10

Commentaire de ghouti253 le 21/09/2007 13:46:49

bon tutorial mais il fallait peut-être juste mettre le lien vers le site de developpez.com ou la doc existe déja, pas besoin de faire un copier coller....

Commentaire de rom1v le 21/09/2007 16:32:07

Je l'ai mise en même temps sur developpez et ici... Pourquoi faire un lien de l'un vers l'autre?

Commentaire de Loda le 24/09/2007 18:12:42

salut,

bon tuto! très bonne approche!

cependant,
il me semble que la ligne
Lock l =   new   ReentrantLock () ;
l.lock();
prêt a confusion. lock devrait être membre de la class (et créer dans la constructeur) et non var local d'une méthode. Ceci est clair dans le rest du tuto, mais il me semble préférable d'éviter les  points pouvant être mal interprétés lorsque l'on traite d'un sujet aussi difficile d'accès.

aussi, l'exemple avec les Condition ne m'as pas vraiment éclairé.

sinon, le reste est génial.

a+

Loda

Commentaire de sofianeannabi le 17/12/2007 16:15:55

super mais monque un P de pratique mais mieux que rien

Commentaire de gouessej le 24/01/2008 22:40:37

10/10, très bon tutorial. Je me dis que si certains de mes collègues l'avaient lu, ils écriraient moins d'anneries.

Commentaire de chatmar le 07/06/2008 14:45:43

Merci pour ce tuto.
Je viens de coder l'exercice du schéma 1 avec une surprenante aisance.
J'ai enfin bien compris le principe sans produire du code-perroquet !

Commentaire de hamidmx le 18/07/2008 03:36:14

Great thanks

Commentaire de diackballa le 08/10/2008 21:07:39

Bien dans l'ensemble touche à des généralités pour avoir une idée sur le sujet

Commentaire de aissam36 le 06/01/2009 11:21:27

Super tuto. Grand merci

Commentaire de kakashady le 01/01/2011 13:26:40

m6 je veux un executable pour un programme de ce genre .

Commentaire de saidfcm le 20/01/2011 09:10:11

Salut;
     J'ai une petite problématique sur un TP de système d'exploitaion alors j'espère que tous les gens qui connaissent la solution qui va me demande; et le voilà le TP:
         Trois types de processus accèdent à une liste partagée :
- ceux qui font de la recherche (S)
- ceux qui font l'insertion (I)
- ceux qui font l'effacement (D)
- Les processus S examinent la liste, donc ils peuvent le faire de façon concurrente entre eux.
- Les processus I ajoutent des nouvelles cellules à la fin de la liste, donc ils doivent être en exclusion
mutuelle entre eux; toutefois, un processus I peut agir en concurrence avec un nombre quelconque de
processus de type S.
- Les processus D effacent des cellules à tout endroit de la liste, donc ils doivent agir en exclusion mutuelle
entre eux et avec les I et S.
Ce problème peut être résolu en plusieurs variantes, en fonction de la priorité qu'on décide de donner à
chaque type de processus :   Variante  : Priorité(D)> Priorité(S)>Priorité(I)
*Travail à faire : Ecrire un programme solution en java de la variante choisie. Le programme doit permettre de:
° Créer et de terminer interactivement des threads (S, I, D).
° De visualiser (graphiquement) l'évolution des tous les threads.

Commentaire de abo3ab le 24/05/2011 09:40:06

merci !!

Commentaire de lycanges le 10/02/2012 15:21:23

Merci beaucoup, même un newbe comme moi a tout comprit.
:)

 Ajouter un commentaire




Nos sponsors


Sondage...

CalendriCode

Février 2012
LMMJVSD
  12345
6789101112
13141516171819
20212223242526
272829    

Consulter la suite du CalendriCode

Photothèque

 
Développement réalisé par Nicolas SOREL (Nix) avec l'aide de : Cyril DURAND et Emmanuel (EBArtSoft), Merci à Vincent pour ses précieux conseils.
CodeS-SourceS.com© Toute reproduction même partielle est interdite sauf accord écrit du Webmaster
CodeS-SourceS.com© est une marque déposée tous droits réservés

Google Coop CodeS-SourceS Google Coop CodeS-SourceS
Temps d'éxécution de la page : 0,499 sec (3)

Nous contacter | Annoncer sur CodeS-SourceS | Mentions légales