You are on page 1of 398

Programmation concurrente en

Rfrence

Java

Brian Goetz
avec Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes et Doug Lea

Rseaux et tlcom Programmation

Gnie logiciel

Scurit Systme dexploitation

Programmation concurrente en Java


Brian Goetz
Avec la collaboration de : Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes et Doug Lea

Traduction : ric Jacoboni Relecture technique : ric Hbert, Architecte Java JEE Nicolas de Loof, Architecte Java

Pearson Education France a apport le plus grand soin la ralisation de ce livre an de vous fournir une information complte et able. Cependant, Pearson Education France nassume de responsabilits, ni pour son utilisation, ni pour les contrefaons de brevets ou atteintes aux droits de tierces personnes qui pourraient rsulter de cette utilisation. Les exemples ou les programmes prsents dans cet ouvrage sont fournis pour illustrer les descriptions thoriques. Ils ne sont en aucun cas destins une utilisation commerciale ou professionnelle. Pearson Education France ne pourra en aucun cas tre tenu pour responsable des prjudices ou dommages de quelque nature que ce soit pouvant rsulter de lutilisation de ces exemples ou programmes. Tous les noms de produits ou marques cits dans ce livre sont des marques dposes par leurs propritaires respectifs.

Publi par Pearson Education France 47 bis, rue des Vinaigriers 75010 PARIS Tl. : 01 72 74 90 00 www.pearson.fr Mise en pages : TyPAO ISBN : 978-2-7440-4109-9 Copyright 2009 Pearson Education France Tous droits rservs

Titre original : Java Concurrency in Practice Traduit de lamricain par ric Jacoboni ISBN original : 978-0-321-34960-6 Copyright 2006 by Pearson Education, Inc All rights reserved dition originale publie par Addison-Wesley Professional, 800 East 96th Street, Indianapolis, Indiana 46240 USA

Aucune reprsentation ou reproduction, mme partielle, autre que celles prvues larticle L. 122-5 2 et 3 a) du code de la proprit intellectuelle ne peut tre faite sans lautorisation expresse de Pearson Education France ou, le cas chant, sans le respect des modalits prvues larticle L. 122-10 dudit code. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

Table des matires


Table des listings ................................................................................................................ Prface ................................................................................................................................ Prface ldition franaise ............................................................................................. Prsentation de louvrage ................................................................................................. Structure de louvrage ................................................................................................ Exemples de code ....................................................................................................... Remerciements ........................................................................................................... 1 Introduction .................................................................................................................. 1.1 Bref historique de la programmation concurrente .......................................... 1.2 Avantages des threads ..................................................................................... 1.2.1 Exploitation de plusieurs processeurs ............................................... 1.2.2 Simplicit de la modlisation ............................................................ 1.2.3 Gestion simplie des vnements asynchrones .............................. 1.2.4 Interfaces utilisateur plus ractives ................................................... 1.3 Risques des threads ......................................................................................... 1.3.1 Risques concernant la "thread safety" ............................................... 1.3.2 Risques sur la vivacit ....................................................................... 1.3.3 Risques sur les performances ............................................................ 1.4 Les threads sont partout .................................................................................. XI XIX XXI XXIII XXIII XXV XXVI 1 1 3 3 4 5 5 6 6 9 9 10

Partie I Les bases 2 Thread safety ................................................................................................................ 2.1 Quest-ce que la thread safety ? ...................................................................... 2.1.1 Exemple : une servlet sans tat ......................................................... 2.2 Atomicit ......................................................................................................... 2.2.1 Situations de comptition .................................................................. 15 17 19 19 21

IV

Table des matires

2.2.2

2.3

2.4 2.5

Exemple : situations de comptition dans une initialisation paresseuse .......................................................................................... 2.2.3 Actions composes ............................................................................ Verrous ............................................................................................................ 2.3.1 Verrous internes ................................................................................. 2.3.2 Rentrance ......................................................................................... Protection de ltat avec les verrous ................................................................ Vivacit et performances .................................................................................

22 23 24 26 27 28 31 35 35 37 38 39 40 42 44 45 46 46 47 49 51 51 53 53 54 55 56 57 57 59 59 60 61 62 63 65 66

3 Partage des objets ......................................................................................................... 3.1 Visibilit .......................................................................................................... 3.1.1 Donnes obsoltes ............................................................................. 3.1.2 Oprations 64 bits non atomiques ..................................................... 3.1.3 Verrous et visibilit ........................................................................... 3.1.4 Variables volatiles ............................................................................. 3.2 Publication et fuite .......................................................................................... 3.2.1 Pratiques de construction sres ......................................................... 3.3 Connement des objets ................................................................................... 3.3.1 Connement ad hoc .......................................................................... 3.3.2 Connement dans la pile ................................................................... 3.3.3 ThreadLocal ..................................................................................... 3.4 Objets non modiables .................................................................................... 3.4.1 Champs final ................................................................................. 3.4.2 Exemple : utilisation de volatile pour publier des objets non modiables ................................................................................. 3.5 Publication sre ............................................................................................... 3.5.1 Publication incorrecte : quand les bons objets deviennent mauvais . 3.5.2 Objets non modiables et initialisation sre ..................................... 3.5.3 Idiomes de publication correcte ........................................................ 3.5.4 Objets non modiables dans les faits ................................................ 3.5.5 Objets modiables ............................................................................ 3.5.6 Partage dobjets de faon sre ........................................................... 4 Composition dobjets ................................................................................................... 4.1 Conception dune classe thread-safe ............................................................... 4.1.1 Exigences de synchronisation ........................................................... 4.1.2 Oprations dpendantes de ltat ...................................................... 4.1.3 Appartenance de ltat ...................................................................... 4.2 Connement des instances .............................................................................. 4.2.1 Le patron moniteur de Java ............................................................... 4.2.2 Exemple : gestion dune otte de vhicules ......................................

Table des matires

4.3

4.4

4.5

Dlgation de la thread safety ......................................................................... 4.3.1 Exemple : gestionnaire de vhicules utilisant la dlgation ............. 4.3.2 Variables dtat indpendantes .......................................................... 4.3.3 checs de la dlgation ..................................................................... 4.3.4 Publication des variables dtat sous-jacentes .................................. 4.3.5 Exemple : gestionnaire de vhicules publiant son tat ..................... Ajout de fonctionnalits des classes thread-safe existantes ............................ 4.4.1 Verrouillage ct client ..................................................................... 4.4.2 Composition ...................................................................................... Documentation des politiques de synchronisation .......................................... 4.5.1 Interprtation des documentations vagues ........................................

67 69 70 71 73 73 75 76 78 78 80 83 83 83 86 87 88 89 91 91 92 94 96 97 97 99 99 101 103 105 107 113

5 Briques de base ............................................................................................................. 5.1 Collections synchronises ............................................................................... 5.1.1 Problmes avec les collections synchronises .................................. 5.1.2 Itrateurs et ConcurrentModificationException ........................ 5.1.3 Itrateurs cachs ................................................................................ 5.2 Collections concurrentes ................................................................................. 5.2.1 ConcurrentHashMap ......................................................................... 5.2.2 Oprations atomiques supplmentaires sur les Map .......................... 5.2.3 CopyOnWriteArrayList ................................................................... 5.3 Files bloquantes et patron producteur-consommateur .................................... 5.3.1 Exemple : indexation des disques ..................................................... 5.3.2 Connement en srie ......................................................................... 5.3.3 Classe Deque et vol de tches ........................................................... 5.4 Mthodes bloquantes et interruptions ............................................................. 5.5 Synchronisateurs ............................................................................................. 5.5.1 Loquets .............................................................................................. 5.5.2 FutureTask ....................................................................................... 5.5.3 Smaphores ....................................................................................... 5.5.4 Barrires ............................................................................................ 5.6 Construction dun cache efcace et adaptable ................................................ Rsum de la premire partie...........................................................................................

Partie II Structuration des applications concurrentes 6 Excution des tches..................................................................................................... 6.1 Excution des tches dans les threads ............................................................. 6.1.1 Excution squentielle des tches ..................................................... 117 117 118

VI

Table des matires

6.1.2 Cration explicite de threads pour les tches .................................... 6.1.3 Inconvnients dune cration illimite de threads ............................. 6.2 Le framework Executor ................................................................................. 6.2.1 Exemple : serveur web utilisant Executor ....................................... 6.2.2 Politiques dexcution ....................................................................... 6.2.3 Pools de threads ................................................................................ 6.2.4 Cycle de vie dun Executor ............................................................. 6.2.5 Tches diffres et priodiques ......................................................... 6.3 Trouver un paralllisme exploitable ................................................................ 6.3.1 Exemple : rendu squentiel dune page ............................................ 6.3.2 Tches partielles : Callable et Future ............................................ 6.3.3 Exemple : afchage dune page avec Future ................................... 6.3.4 Limitations du paralllisme de tches htrognes ........................... 6.3.5 CompletionService : quand Executor rencontre BlockingQueue 6.3.6 Exemple : afchage dune page avec CompletionService ............. 6.3.7 Imposer des dlais aux tches ........................................................... 6.3.8 Exemple : portail de rservations ...................................................... Rsum ....................................................................................................................... 7 Annulation et arrt ....................................................................................................... 7.1 Annulation des tches ..................................................................................... 7.1.1 Interruption ........................................................................................ 7.1.2 Politiques dinterruption ................................................................... 7.1.3 Rpondre aux interruptions ............................................................... 7.1.4 Exemple : excution avec dlai ......................................................... 7.1.5 Annulation avec Future ................................................................... 7.1.6 Mthodes bloquantes non interruptibles ........................................... 7.1.7 Encapsulation dune annulation non standard avec newTaskFor() 7.2 Arrt dun service reposant sur des threads .................................................... 7.2.1 Exemple : service de journalisation .................................................. 7.2.2 Mthodes darrt de ExecutorService ........................................... 7.2.3 Pilules empoisonnes ........................................................................ 7.2.4 Exemple : un service dexcution phmre ..................................... 7.2.5 Limitations de shutdownNow() ........................................................ 7.3 Gestion de la n anormale dun thread ........................................................... 7.3.1 Gestionnaires dexceptions non captures ........................................ 7.4 Arrt de la JVM ............................................................................................... 7.4.1 Mthodes dinterception dun ordre darrt ...................................... 7.4.2 Threads dmons ................................................................................ 7.4.3 Finaliseurs ......................................................................................... Rsum .......................................................................................................................

119 120 121 122 123 124 125 127 128 129 129 131 132 133 134 135 136 137 139 140 142 145 146 148 150 151 153 154 155 158 159 160 161 163 165 166 166 168 168 169

Table des matires

VII

8 Pools de threads ............................................................................................................ 8.1 Couplage implicite entre les tches et les politiques dexcution ................... 8.1.1 Interblocage par famine de thread ..................................................... 8.1.2 Tches longues .................................................................................. 8.2 Taille des pools de threads .............................................................................. 8.3 Conguration de ThreadPoolExecutor ......................................................... 8.3.1 Cration et suppression de threads .................................................... 8.3.2 Gestion des tches en attente ............................................................ 8.3.3 Politiques de saturation ..................................................................... 8.3.4 Fabriques de threads .......................................................................... 8.3.5 Personnalisation de ThreadPoolExecutor aprs sa construction .... 8.4 Extension de ThreadPoolExecutor ............................................................... 8.4.1 Exemple : ajout de statistiques un pool de threads ........................ 8.5 Paralllisation des algorithmes rcursifs ......................................................... 8.5.1 Exemple : un framework de jeu de rexion .................................... Rsum ....................................................................................................................... 9 Applications graphiques .............................................................................................. 9.1 Pourquoi les interfaces graphiques sont-elles monothreads ? ............................ 9.1.1 Traitement squentiel des vnements .............................................. 9.1.2 Connement aux threads avec Swing ............................................... 9.2 Tches courtes de linterface graphique .......................................................... 9.3 Tches longues de linterface graphique ......................................................... 9.3.1 Annulation ......................................................................................... 9.3.2 Indication de progression et de terminaison ..................................... 9.3.3 SwingWorker .................................................................................... 9.4 Modles de donnes partages ........................................................................ 9.4.1 Modles de donnes thread-safe ....................................................... 9.4.2 Modles de donnes spars ............................................................. 9.5 Autres formes de sous-systmes monothreads ................................................ Rsum .......................................................................................................................

171 171 173 174 174 176 176 177 179 181 183 183 184 185 187 192 193 193 195 196 198 199 201 202 204 204 205 205 206 206

Partie III Vivacit, performances et tests 10 viter les problmes de vivacit ................................................................................ 10.1 Interblocages (deadlock) ................................................................................ 10.1.1 Interblocages lis lordre du verrouillage ....................................... 10.1.2 Interblocages dynamiques lis lordre du verrouillage .................. 10.1.3 Interblocages entre objets coopratifs ............................................... 209 209 210 212 215

VIII

Table des matires

10.1.4 Appels ouverts ................................................................................... 10.1.5 Interblocages lis aux ressources ...................................................... 10.2 viter et diagnostiquer les interblocages ......................................................... 10.2.1 Tentatives de verrouillage avec expiration ........................................ 10.2.2 Analyse des interblocages avec les traces des threads ...................... 10.3 Autres problmes de vivacit .......................................................................... 10.3.1 Famine ............................................................................................... 10.3.2 Faible ractivit ................................................................................. 10.3.3 Livelock ............................................................................................. Rsum ....................................................................................................................... 11 Performances et adaptabilit..................................................................................... 11.1 Penser aux performances ................................................................................. 11.1.1 Performances et adaptabilit ............................................................. 11.1.2 Compromis sur lvaluation des performances ................................. 11.2 La loi dAmdahl .............................................................................................. 11.2.1 Exemple : srialisation cache dans les frameworks ........................ 11.2.2 Application qualitative de la loi dAmdahl ....................................... 11.3 Cots lis aux threads ...................................................................................... 11.3.1 Changements de contexte .................................................................. 11.3.2 Synchronisation de la mmoire ......................................................... 11.3.3 Blocages ............................................................................................ 11.4 Rduction de la comptition pour les verrous ................................................. 11.4.1 Rduction de la porte des verrous ("entrer, sortir") ......................... 11.4.2 Rduire la granularit du verrouillage .............................................. 11.4.3 Dcoupage du verrouillage ............................................................... 11.4.4 viter les points chauds ..................................................................... 11.4.5 Alternatives aux verrous exclusifs .................................................... 11.4.6 Surveillance de lutilisation du processeur ....................................... 11.4.7 Dire non aux pools dobjets .............................................................. 11.5 Exemple : comparaison des performances des Map ......................................... 11.6 Rduction du surcot des changements de contexte ....................................... Rsum ....................................................................................................................... 12 Tests des programmes concurrents........................................................................... 12.1 Tests de la justesse .......................................................................................... 12.1.1 Tests unitaires de base ....................................................................... 12.1.2 Tests des oprations bloquantes ........................................................ 12.1.3 Test de la scurit vis--vis des threads ............................................ 12.1.4 Test de la gestion des ressources ....................................................... 12.1.5 Utilisation des fonctions de rappel .................................................... 12.1.6 Production dentrelacements supplmentaires ..................................

216 218 219 219 220 221 222 223 223 224 225 225 226 228 230 232 233 234 234 235 237 237 238 240 242 244 245 245 246 247 249 251 253 254 256 256 258 263 264 265

Table des matires

IX

12.2

Tests de performances ..................................................................................... 12.2.1 Extension de PutTakeTest pour ajouter un timing .......................... 12.2.2 Comparaison de plusieurs algorithmes ............................................. 12.2.3 Mesure de la ractivit ...................................................................... 12.3 Piges des tests de performance ...................................................................... 12.3.1 Ramasse-miettes ................................................................................ 12.3.2 Compilation dynamique .................................................................... 12.3.3 chantillonnage irraliste de portions de code ................................. 12.3.4 Degrs de comptition irralistes ...................................................... 12.3.5 limination du code mort .................................................................. 12.4 Approches de tests complmentaires .............................................................. 12.4.1 Relecture du code .............................................................................. 12.4.2 Outils danalyse statiques .................................................................. 12.4.3 Techniques de tests orientes aspects ................................................ 12.4.4 Proleurs et outils de surveillance .................................................... Rsum .......................................................................................................................

266 266 269 270 272 272 272 274 274 275 277 277 278 279 280 280

Partie IV Sujets avancs 13 Verrous explicites........................................................................................................ 13.1 Lock et ReentrantLock .................................................................................. 13.1.1 Prise de verrou scrutable et avec dlai .............................................. 13.1.2 Prise de verrou interruptible .............................................................. 13.1.3 Verrouillage non structur en bloc .................................................... 13.2 Remarques sur les performances ..................................................................... 13.3 quit .............................................................................................................. 13.4 synchronized vs. ReentrantLock ................................................................ 13.5 Verrous en lecture-criture .............................................................................. Rsum ....................................................................................................................... 14 Construction de synchronisateurs personnaliss..................................................... 14.1 Gestion de la dpendance par rapport ltat ................................................. 14.1.1 Exemple : propagation de lchec de la prcondition aux appelants 14.1.2 Exemple : blocage brutal par essai et mise en sommeil .................... 14.1.3 Les les dattente de condition ......................................................... 14.2 Utilisation des les dattente de condition ...................................................... 14.2.1 Le prdicat de condition .................................................................... 14.2.2 Rveil trop prcoce ........................................................................... 283 283 285 287 287 288 289 291 292 296 297 297 299 301 303 304 304 306

Table des matires

14.2.3 Signaux manqus .............................................................................. 14.2.4 Notication ....................................................................................... 14.2.5 Exemple : une classe "porte dentre" .............................................. 14.2.6 Problmes de safety des sous-classes ................................................ 14.2.7 Encapsulation des les dattente de condition .................................. 14.2.8 Protocoles dentre et de sortie ......................................................... 14.3 Objets conditions explicites ............................................................................ 14.4 Anatomie dun synchronisateur ...................................................................... 14.5 AbstractQueuedSynchronizer ..................................................................... 14.5.1 Un loquet simple ............................................................................... 14.6 AQS dans les classes de java.util.concurrent ......................................... 14.6.1 ReentrantLock ................................................................................. 14.6.2 Semaphore et CountDownLatch ........................................................ 14.6.3 FutureTask ....................................................................................... 14.6.4 ReentrantReadWriteLock ............................................................... Rsum ....................................................................................................................... 15 Variables atomiques et synchronisation non bloquante.......................................... 15.1 Inconvnients du verrouillage ......................................................................... 15.2 Support matriel de la concurrence ................................................................. 15.2.1 Linstruction Compare-and-swap ..................................................... 15.2.2 Compteur non bloquant ..................................................................... 15.2.3 Support de CAS dans la JVM ........................................................... 15.3 Classes de variables atomiques ....................................................................... 15.3.1 Variables atomiques comme "volatiles amliores" ......................... 15.3.2 Comparaison des performances des verrous et des variables atomiques ................................................................. 15.4 Algorithmes non bloquants ............................................................................. 15.4.1 Pile non bloquante ............................................................................. 15.4.2 File chane non bloquante ............................................................... 15.4.3 Modicateurs atomiques de champs ................................................. 15.4.4 Le problme ABA ............................................................................. Rsum ....................................................................................................................... 16 Le modle mmoire de Java....................................................................................... 16.1 Quest-ce quun modle mmoire et pourquoi en a-t-on besoin ? ............................................................................................ 16.1.1 Modles mmoire des plates-formes ................................................. 16.1.2 Rorganisation .................................................................................. 16.1.3 Le modle mmoire de Java en moins de cinq cents mots ............... 16.1.4 Tirer parti de la synchronisation .......................................................

307 308 310 311 312 312 313 315 317 319 320 320 321 322 322 323 325 326 327 328 329 331 331 332 333 336 337 338 342 342 343 345 345 346 347 349 351

Table des matires

XI

16.2

Publication ....................................................................................................... 16.2.1 Publication incorrecte ....................................................................... 16.2.2 Publication correcte ........................................................................... 16.2.3 Idiomes de publication correcte ........................................................ 16.2.4 Verrouillage contrl deux fois ......................................................... 16.3 Initialisation sre ............................................................................................. Rsum ....................................................................................................................... Annexe A.1 A.2 Annotations pour la concurrence.................................................................. Annotations de classes .................................................................................... Annotations de champs et de mthodes ..........................................................

353 353 354 355 356 358 359 361 361 361 363 365

Bibliographie...................................................................................................................... Index ...................................................................................................................................

Table des listings


Listing 1 : Mauvaise faon de trier une liste. Ne le faites pas. ........................................... Listing 2 : Mthode peu optimale de trier une liste. ........................................................... Listing 1.1 : Gnrateur de squence non thread-safe. ....................................................... Listing 1.2 : Gnrateur de squence thread-safe. .............................................................. Listing 2.1 : Une servlet sans tat. ...................................................................................... Listing 2.2 : Servlet comptant le nombre de requtes sans la synchronisation ncessaire. Ne le faites pas. ........................................................................... Listing 2.3 : Situation de comptition dans une initialisation paresseuse. Ne le faites pas. Listing 2.4 : Servlet comptant les requtes avec AtomicLong. .......................................... Listing 2.5 : Servlet tentant de mettre en cache son dernier rsultat sans latomicit adquate. Ne le faites pas. .............................................................................. Listing 2.6 : Servlet mettant en cache le dernier rsultat, mais avec une trs mauvaise concurrence. Ne le faites pas. ........................................................................ Listing 2.7 : Ce code se bloquerait si les verrous internes ntaient pas rentrants. .......... Listing 2.8 : Servlet mettant en cache la dernire requte et son rsultat. ......................... Listing 3.1 : Partage de donnes sans synchronisation. Ne le faites pas. ........................... Listing 3.2 : Conteneur non thread-safe pour un entier modiable. ................................... Listing 3.3 : Conteneur thread-safe pour un entier modiable. .......................................... Listing 3.4 : Compter les moutons. .................................................................................... Listing 3.5 : Publication dun objet. ................................................................................... Listing 3.6 : Ltat modiable interne la classe peut schapper. Ne le faites pas. ......... Listing 3.7 : Permet implicitement la rfrence this de schapper. Ne le faites pas. ..... Listing 3.8 : Utilisation dune mthode fabrique pour empcher la rfrence this de schapper au cours de la construction de lobjet. .................................... Listing 3.9 : Connement des variables locales, de types primitifs ou de types rfrences. Listing 3.10 : Utilisation de ThreadLocal pour garantir le connement au thread. .......... Listing 3.11 : Classe non modiable construite partir dobjets modiables sous-jacents. Listing 3.12 : Conteneur non modiable pour mettre en cache un nombre et ses facteurs. Listing 3.13 : Mise en cache du dernier rsultat laide dune rfrence volatile vers un objet conteneur non modiable. ............................................................. Listing 3.14 : Publication dun objet sans synchronisation approprie. Ne le faites pas. .... Listing 3.15 : Classe risquant un problme si elle nest pas correctement publie. ........... XX XX 7 8 19 20 22 24 25 27 28 32 36 38 38 41 42 42 43 44 47 48 50 52 52 53 54

XIV

Table des listings

Listing 4.1 : Compteur mono-thread utilisant le patron moniteur de Java. ........................ Listing 4.2 : Utilisation du connement pour assurer la thread safety. .............................. Listing 4.3 : Protection de ltat laide dun verrou priv. .............................................. Listing 4.4 : Implmentation du gestionnaire de vhicule reposant sur un moniteur. ........ Listing 4.5 : Classe Point modiable ressemblant java.awt.Point. ........................... Listing 4.6 : Classe Point non modiable utilise par DelegatingVehicleTracker. .... Listing 4.7 : Dlgation de la thread safety un objet ConcurrentHashMap. ................... Listing 4.8 : Renvoi dune copie statique de lensemble des emplacements au lieu dune copie "vivante". .................................................................................... Listing 4.9 : Dlgation de la thread plusieurs variables dtat sous-jacentes. ............... Listing 4.10 : Classe pour des intervalles numriques, qui ne protge pas sufsamment ses invariants. Ne le faites pas. .................................................................... Listing 4.11 : Classe point modiable et thread-safe. ...................................................... Listing 4.12 : Gestionnaire de vhicule qui publie en toute scurit son tat interne. ....... Listing 4.13 : Extension de Vector pour disposer dune mthode ajouter-si-absent. ....... Listing 4.14 : Tentative non thread-safe dimplmenter ajouter-si-absent. Ne le faites pas. Listing 4.15 : Implmentation dajouter-si-absent avec un verrouillage ct client. ......... Listing 4.16 : Implmentation dajouter-si-absent en utilisant la composition. ................ Listing 5.1 : Actions composes sur un Vector pouvant produire des rsultats inattendus. Listing 5.2 : Actions composes sur Vector utilisant un verrouillage ct client. ............ Listing 5.3 : Itration pouvant dclencher ArrayIndexOutOfBoundsException. ............ Listing 5.4 : Itration avec un verrouillage ct client. ...................................................... Listing 5.5 : Parcours dun objet List avec un Iterator. ................................................ Listing 5.6 : Itration cache dans la concatnation des chanes. Ne le faites pas. ............ Listing 5.7 : Interface ConcurrentMap. ............................................................................. Listing 5.8 : Tches producteur et consommateur dans une application dindexation des chiers. .................................................................................................... Listing 5.9 : Lancement de lindexation. ............................................................................ Listing 5.10 : Restauration de ltat dinterruption an de ne pas absorber linterruption. Listing 5.11 : Utilisation de la classe CountDownLatch pour lancer et stopper des threads et mesurer le temps dexcution. .............................................. Listing 5.12 : Utilisation de FutureTask pour prcharger des donnes dont on aura besoin plus tard. ...................................................................... Listing 5.13 : Coercition dun objet Throwable non contrl en RuntimeException. .... Listing 5.14 : Utilisation dun Semaphore pour borner une collection. ............................. Listing 5.15 : Coordination des calculs avec CyclicBarrier pour une simulation de cellules. Listing 5.16 : Premire tentative de cache, utilisant HashMap et la synchronisation. ......... Listing 5.17 : Remplacement de HashMap par ConcurrentHashMap. ................................ Listing 5.18 : Enveloppe de mmosation utilisant FutureTask. ......................................

60 64 66 68 69 69 69 70 71 71 73 74 76 76 77 78 84 85 85 86 86 88 91 95 96 99 101 102 103 104 106 107 109 110

Table des listings

XV

Listing 5.19 : Implmentation nale de Memoizer. ............................................................ Listing 5.20 : Servlet de factorisation mettant en cache ses rsultats avec Memoizer. ...... Listing 6.1 : Serveur web squentiel. ................................................................................. Listing 6.2 : Serveur web lanant un thread par requte. ................................................... Listing 6.3 : Interface Executor. ........................................................................................ Listing 6.4 : Serveur web utilisant un pool de threads. ...................................................... Listing 6.5 : Executor lanant un nouveau thread pour chaque tche. ............................. Listing 6.6 : Executor excutant les tches de faon synchrone dans le thread appelant. Listing 6.7 : Mthodes de ExecutorService pour le cycle de vie. ................................... Listing 6.8 : Serveur web avec cycle de vie. ...................................................................... Listing 6.9 : Classe illustrant le comportement confus de Timer. ...................................... Listing 6.10 : Afchage squentiel des lments dune page. ............................................ Listing 6.11 : Interfaces Callable et Future. ................................................................... Listing 6.12 : Implmentation par dfaut de newTaskFor() dans ThreadPoolExecutor. Listing 6.13 : Attente du tlchargement dimage avec Future. ....................................... Listing 6.14 : La classe QueueingFuture utilise par ExecutorCompletionService. ..... Listing 6.15 : Utilisation de CompletionService pour afcher les lments de la page ds quils sont disponibles. .......................................................................... Listing 6.16 : Rcupration dune publicit dans un dlai imparti. ................................... Listing 6.17 : Obtention de tarifs dans un dlai imparti. .................................................... Listing 7.1 : Utilisation dun champ volatile pour stocker ltat dannulation. ............. Listing 7.2 : Gnration de nombres premiers pendant une seconde. ................................ Listing 7.3 : Annulation non able pouvant bloquer les producteurs. Ne le faites pas. ..... Listing 7.4 : Mthodes dinterruption de Thread. .............................................................. Listing 7.5 : Utilisation dune interruption pour lannulation. ........................................... Listing 7.6 : Propagation de InterruptedException aux appelants. ............................... Listing 7.7 : Tche non annulable qui restaure linterruption avant de se terminer. .......... Listing 7.8 : Planication dune interruption sur un thread emprunt. Ne le faites pas. .... Listing 7.9 : Interruption dune tche dans un thread ddi. .............................................. Listing 7.10 : Annulation dune tche avec Future. .......................................................... Listing 7.11 : Encapsulation des annulations non standard dans un thread par rednition de interrupt(). ................................................................ Listing 7.12 : Encapsulation des annulations non standard avec newTaskFor(). ............. Listing 7.13 : Service de journalisation producteur-consommateur sans support de larrt. Listing 7.14 : Moyen non able dajouter larrt au service de journalisation. ................. Listing 7.15 : Ajout dune annulation able LogWriter. ................................................ Listing 7.16 : Service de journalisation utilisant un ExecutorService. ........................... Listing 7.17 : Arrt dun service avec une pilule empoisonne. .........................................

111 112 118 119 121 122 123 123 126 126 128 129 130 131 131 134 134 135 137 140 141 142 143 144 147 147 149 149 151 152 154 156 157 157 158 159

XVI

Table des listings

Listing 7.18 : Thread producteur pour IndexingService. ................................................. Listing 7.19 : Thread consommateur pour IndexingService. ......................................... Listing 7.20 : Utilisation dun Executor priv dont la dure de vie est limite un appel de mthode. .................................................................................................. Listing 7.21 : ExecutorService mmorisant les tches annules aprs larrt. ............... Listing 7.22 : Utilisation de TrackingExecutorService pour mmoriser les tches non termines an de les relancer plus tard. ................................................ Listing 7.23 : Structure typique dun thread dun pool de threads. .................................... Listing 7.24 : Interface UncaughtExceptionHandler. ...................................................... Listing 7.25 : UncaughtExceptionHandler, qui inscrit lexception dans le journal. ....... Listing 7.26 : Enregistrement dun hook darrt pour arrter le service de journalisation. Listing 8.1 : Interblocage de tches dans un Executor monothread. Ne le faites pas. ...... Listing 8.2 : Constructeur gnral de ThreadPoolExecutor. ............................................ Listing 8.3 : Cration dun pool de threads de taille xe avec une le borne et la politique de saturation caller-runs. .................................................... Listing 8.4 : Utilisation dun Semaphore pour ralentir la soumission des tches. ............. Listing 8.5 : Interface ThreadFactory. .............................................................................. Listing 8.6 : Fabrique de threads personnaliss. ................................................................. Listing 8.7 : Classe de base pour les threads personnaliss. .............................................. Listing 8.8 : Modication dun Executor cr avec les mthodes fabriques standard. ..... Listing 8.9 : Pool de threads tendu par une journalisation et une mesure du temps. ........ Listing 8.10 : Transformation dune excution squentielle en excution parallle. ......... Listing 8.11 : Transformation dune rcursion terminale squentielle en rcursion parallle. Listing 8.12 : Attente des rsultats calculs en parallle. ................................................... Listing 8.13 : Abstraction pour les jeux de type "taquin". .................................................. Listing 8.14 : Nud pour le framework de rsolution des jeux de rexion. .................... Listing 8.15 : Rsolveur squentiel dun puzzle. ............................................................... Listing 8.16 : Version parallle du rsolveur de puzzle. ..................................................... Listing 8.17 : Loquet de rsultat partiel utilis par ConcurrentPuzzleSolver. ............... Listing 8.18 : Rsolveur reconnaissant quil ny a pas de solution. ................................... Listing 9.1 : Implmentation de SwingUtilities laide dun Executor. ..................... Listing 9.2 : Executor construit au-dessus de SwingUtilities. ....................................... Listing 9.3 : couteur dvnement simple. ....................................................................... Listing 9.4 : Liaison dune tche longue un composant visuel. ...................................... Listing 9.5 : Tche longue avec effet visuel. ...................................................................... Listing 9.6 : Annulation dune tche longue. ..................................................................... Listing 9.7 : Classe de tche en arrire-plan supportant lannulation, ainsi que la notication de terminaison et de progression. ............................

159 160 160 161 162 164 165 165 167 173 176 180 180 181 182 182 183 184 185 186 187 187 187 188 189 190 191 197 197 198 200 200 201 202

Table des listings

XVII

Listing 9.8 : Utilisation de BackgroundTask pour lancer une tche longue et annulable. Listing 10.1 : Interblocage simple li lordre du verrouillage. Ne le faites pas. ............. Listing 10.2 : Interblocage dynamique li lordre du verrouillage. Ne le faites pas. ...... Listing 10.3 : Induire un ordre de verrouillage pour viter les interblocages. ................... Listing 10.4 : Boucle provoquant un interblocage dans une situation normale. ................ Listing 10.5 : Interblocage li lordre du verrouillage entre des objets coopratifs. Ne le faites pas. ............................................................................................ Listing 10.6 : Utilisation dappels ouverts pour viter linterblocage entre des objets coopratifs. .................................................................................. Listing 10.7 : Portion dune trace de thread aprs un interblocage. ................................... Listing 11.1 : Accs squentiel une le dattente. ........................................................... Listing 11.2 : Synchronisation inutile. Ne le faites pas. ..................................................... Listing 11.3 : Candidat llision de verrou. ..................................................................... Listing 11.4 : Dtention dun verrou plus longtemps que ncessaire. ................................ Listing 11.5 : Rduction de la dure du verrouillage. ........................................................ Listing 11.6 : Candidat au dcoupage du verrou. ............................................................... Listing 11.7 : Modication de ServerStatus pour utiliser des verrous diviss. .............. Listing 11.8 : Hachage utilisant le dcoupage du verrouillage. ......................................... Listing 12.1 : Tampon born utilisant la classe Semaphore. .............................................. Listing 12.2 : Tests unitaires de base pour BoundedBuffer. .............................................. Listing 12.3 : Test du blocage et de la rponse une interruption. .................................... Listing 12.4 : Gnrateur de nombre alatoire de qualit moyenne mais sufsante pour les tests. ............................................................................................... Listing 12.5 : Programme de test producteur-consommateur pour BoundedBuffer. ........ Listing 12.6 : Classes producteur et consommateur utilises dans PutTakeTest. ............ Listing 12.7 : Test des fuites de ressources. ....................................................................... Listing 12.8 : Fabrique de threads pour tester ThreadPoolExecutor. .............................. Listing 12.9 : Mthode de test pour vrier lexpansion du pool de threads. .................... Listing 12.10 : Utilisation de Thread.yield() pour produire plus dentrelacements ...... Listing 12.11 : Mesure du temps laide dune barrire. ................................................... Listing 12.12 : Test avec mesure du temps laide dune barrire. ................................... Listing 12.13 : Programme pilote pour TimedPutTakeTest. ............................................ Listing 13.1 : Interface Lock. ............................................................................................. Listing 13.2 : Protection de ltat dun objet avec ReentrantLock. .................................. Listing 13.3 : Utilisation de tryLock() pour viter les interblocages dus lordre des verrouillages. ......................................................................................... Listing 13.4 : Verrouillage avec temps imparti. .................................................................. Listing 13.5 : Prise de verrou interruptible. ........................................................................

203 211 212 213 214 215 217 220 231 236 236 239 239 241 242 243 255 256 257 260 260 261 263 264 265 266 267 267 268 283 284 285 286 287

XVIII Table des listings

Listing 13.6 : Interface ReadWriteLock. ........................................................................... Listing 13.7 : Enveloppe dun Map avec un verrou de lecture-criture. .............................. Listing 14.1 : Structure des actions bloquantes en fonction de ltat. ................................ Listing 14.2 : Classe de base pour les implmentations de tampons borns. ..................... Listing 14.3 : Tampon born qui se drobe lorsque les prconditions ne sont pas vries. Listing 14.4 : Code client pour lappel de GrumpyBoundedBuffer. .................................. Listing 14.5 : Tampon born avec blocage brutal. .............................................................. Listing 14.6 : Tampon born utilisant des les dattente de condition. .............................. Listing 14.7 : Forme canonique des mthodes dpendantes de ltat. ............................... Listing 14.8 : Utilisation dune notication conditionnelle dans BoundedBuffer.put(). Listing 14.9 : Porte refermable laide de wait() et notifyAll(). ................................ Listing 14.10 : Interface Condition. ................................................................................ Listing 14.11 : Tampon born utilisant des variables conditions explicites. ...................... Listing 14.12 : Semaphore implment partir de Lock. .................................................. Listing 14.13 : Formes canoniques de lacquisition et de la libration avec AQS. ............ Listing 14.14 : Loquet binaire utilisant AbstractQueuedSynchronizer. ........................ Listing 14.15 : Implmentation de tryAcquire() pour un ReentrantLock non quitable. Listing 14.16 : Les mthodes tryAcquireShared() et tryReleaseShared() de Semaphore. ........................................................................................... Listing 15.1 : Simulation de lopration CAS. ................................................................... Listing 15.2 : Compteur non bloquant utilisant linstruction CAS. ................................... Listing 15.3 : Prservation des invariants multivariables avec CAS. ................................. Listing 15.4 : Gnrateur de nombres pseudo-alatoires avec ReentrantLock. ............... Listing 15.5 : Gnrateur de nombres pseudo-alatoires avec AtomicInteger. ............... Listing 15.6 : Pile non bloquante utilisant lalgorithme de Treiber (Treiber, 1986). ......... Listing 15.7 : Insertion dans lalgorithme non bloquant de Michael-Scott (Michael et Scott, 1996). ............................................................................. Listing 15.8 : Utilisation de modicateurs atomiques de champs dans ConcurrentLinkedQueue. .......................................................................... Listing 16.1 : Programme mal synchronis pouvant produire des rsultats surprenants. Ne le faites pas. ............................................................................................ Listing 16.2 : Classe interne de FutureTask illustrant une mise prot de la synchronisation. Listing 16.3 : Initialisation paresseuse incorrecte. Ne le faites pas. ................................... Listing 16.4 : Initialisation paresseuse thread-safe. ........................................................... Listing 16.5 : Initialisation impatiente. .............................................................................. Listing 16.6 : Idiome de la classe conteneur de linitialisation paresseuse. ....................... Listing 16.7 : Antipatron du verrouillage vri deux fois. Ne le faites pas. ..................... Listing 16.8 : Initialisation sre pour les objets non modiables. ......................................

293 295 298 299 299 300 301 304 307 310 310 313 314 315 318 319 321 322 328 329 333 334 334 337 340 342 348 351 353 355 356 356 357 358

Prface
lheure o ce livre est crit, les machines de gamme moyenne utilisent dsormais des processeurs multicurs. En mme temps, et ce nest pas une concidence, les rapports de bogues signalent de plus en plus de problmes lis aux threads. Dans un article rcent post sur le site des dveloppeurs de NetBeans, lun des dveloppeurs principaux indique quune mme classe a t corrige plus de quatorze fois pour remdier ce genre de problme. Dion Almaer, ancien diteur de TheServerSide, a rcemment crit dans son blog (aprs une session de dbogage harassante qui a ni par rvler un bogue li aux threads) que ce type de bogue est si courant dans les programmes Java que ceux-ci ne fonctionnent souvent que "par accident". Le dveloppement, le test et le dbogage des programmes multithreads peut se rvler trs difcile car, videmment, les problmes de concurrence se manifestent de faon imprvisible. Ils apparaissent gnralement au pire moment lorsque le programme est en production et doit grer une lourde charge de travail. Lune des difcults de la programmation concurrente en Java consiste distinguer la concurrence offerte par la plate-forme et la faon dont les dveloppeurs doivent apprhender cette concurrence dans leurs programmes. Le langage fournit des mcanismes de bas niveau, comme la synchronisation et lattente de conditions, qui doivent tre utiliss correctement pour implmenter des protocoles ou des politiques au niveau des applications. Sans ces politiques, il est vraiment trs facile de crer des programmes qui se compileront et sembleront fonctionner alors quils sont bogus. De nombreux ouvrages excellents consacrs la programmation concurrente manquent ainsi leur but en se consacrant presque exclusivement aux mcanismes de bas niveau et aux API au lieu de sintresser aux politiques et aux patrons de conception. Java 5.0 constitue une tape majeure vers le dveloppement dapplications concurrentes en Java car il fournit la fois des composants de haut niveau et des mcanismes de bas niveau supplmentaires facilitant la construction des applications concurrentes la fois pour les dbutants et les experts. Les auteurs sont des membres essentiels du JCP Expert Group1, qui a cr ces outils ; outre la description de leur comportement et de
1. N.d.T. : JCP signie Java Community Process. Il sagit dun processus de dveloppement et damlioration de Java ouvert toutes les bonnes volonts. Les propositions mises sont appeles JSR (Java Specication Request) et leur mise en place est encadre par un groupe dexperts (Expert Group).

XX

Programmation concurrente en Java

leurs fonctionnalits, nous prsenterons les cas dutilisation qui ont motiv leur ajout aux bibliothques de la plate-forme. Notre but est de fournir aux lecteurs un ensemble de rgles de conception et de modles mentaux qui facilitent et rendent plus agrable le dveloppement de classes et dapplications concurrentes en Java. Nous esprons que vous apprcierez Programmation concurrente en Java. Brian Goetz Williston, VT Mars 2006

Prface ldition franaise


Lors de la premire dition de ce livre, nous avions crit que "les processeurs multicurs commencent tre sufsamment bon march pour apparatre dans les systmes de milieu de gamme". Deux ans plus tard, nous pouvons constater que cette tendance sest poursuivie, voire acclre. Mme les portables et les machines de bureau dentre de gamme disposent maintenant de processeurs multicurs, tandis que les machines de haut de gamme voient leur nombre de curs grandir chaque anne et que les fabricants de CPU ont clairement indiqu quils sattendaient ce que le nombre de curs progresse de faon exponentielle au cours des prochaines annes. Du coup, il devient difcile de trouver des systmes monoprocesseurs. Cette tendance du matriel pose des problmes non ngligeables aux dveloppeurs logiciels. Il ne suft plus dexcuter des programmes existants sur de nouveaux processeurs pour quils aillent plus vite. La loi de Moore continue de dvelopper plus de transistors chaque anne, mais elle nous offre dsormais plus de curs que de curs plus rapides. Si nous voulons tirer parti des avantages de la puissance des nouveaux processeurs, nos programmes doivent tre crits pour supporter les environnements concurrents, ce qui reprsente un d la fois en termes darchitecture, de programmation et de tests. Le but de ce livre est de rpondre ces ds en offrant des techniques, des patrons et des outils pour analyser les programmes concurrents et pour encapsuler la complexit des interactions concurrentes. Comprendre la concurrence est devenu plus que jamais ncessaire pour les dveloppeurs Java. Brian Goetz Williston, VT Janvier 2008

Prsentation de louvrage
Structure de louvrage
Pour viter la confusion entre les mcanismes de bas niveau de Java et les politiques de conception ncessaires, nous prsenterons un ensemble de rgles simpli permettant dcrire des programmes concurrents. En lisant ces rgles, les experts pourront dire : "Hum, ce nest pas entirement vrai : la classe C est thread-safe bien quelle viole la rgle R !" crire des programmes corrects qui ne respectent pas nos rgles est bien sr possible, mais condition de connatre parfaitement les mcanismes de bas niveau du modle mmoire de Java, or nous voulons justement que les dveloppeurs puissent crire des programmes concurrents sans avoir besoin de matriser tous ces dtails. Nos rgles simplies permettent de produire des programmes concurrents corrects et faciles maintenir. Nous supposons que le lecteur connat dj un peu les mcanismes de base de la programmation concurrente en Java. Programmation concurrente en Java nest pas une introduction la programmation concurrente pour cela, consultez le chapitre consacr ce sujet dans un ouvrage qui fait autorit, comme The Java Programming Language (Arnold et al., 2005). Ce nest pas non plus un ouvrage de rfrence sur la concurrence en gnral pour cela, lisez Concurrent Programming in Java (Lea, 2000). Nous prfrons ici offrir des rgles de conceptions pratiques pour aider les dveloppeurs crer des classes concurrentes, sres et efcaces. Lorsque cela sera ncessaire, nous ferons rfrence aux sections appropries de The Java Programming Language, Concurrent Programming in Java, The Java Language Specication (Gosling et al., 2005) et Effective Java (Bloch, 2001) en utilisant les conventions [JPL n.m], [CPJ n.m], [JLS n.m] et [EJ Item n]. Aprs lintroduction (Chapitre 1), ce livre est dcoup en quatre parties. Les bases. La premire partie (Chapitres 2 5) sintresse aux concepts fondamentaux de la concurrence et des threads, ainsi qu la faon de composer des classes "thread-safe" 1 partir des composants fournis par la bibliothque de classes. Une "carte de rfrence" rsume les rgles les plus importantes prsentes dans cette partie.
1. N.d.T : Dans ce livre nous garderons certains termes anglais car ils nont pas dquivalents reconnus en franais. Cest le cas de "thread-safe", qui est une proprit indiquant quun code a t conu pour se comporter correctement lorsquon y accde par plusieurs threads simultanment.

XXIV Programmation concurrente en Java

Les Chapitres 2 (Thread safety) et 3 (Partage des objets) prsentent les concepts fondamentaux de cet ouvrage. Quasiment toutes les rgles lies aux problmes de concurrence, la construction de classes thread-safe et la vrication du bon fonctionnement de ces classes sont introduites dans ces deux chapitres. Si vous prfrez la "pratique" la "thorie", vous pourriez tre tent de passer directement la deuxime partie du livre, mais assurez-vous de lire ces chapitres avant dcrire du code concurrent ! Le Chapitre 4 (Composition dobjets) explique comment composer des classes thread-safe pour former des classes thread-safe plus importantes. Le Chapitre 5 (Briques de base) dcrit les briques de base de la programmation concurrente les collections et les synchronisateurs thread-safe fournies par les bibliothques de la plate-forme. Structuration des applications parallles. La deuxime partie (Chapitres 6 9) explique comment utiliser les threads pour amliorer le rendement ou le temps de rponse des applications concurrentes. Le Chapitre 6 (Excution des tches) montre comment identier les tches paralllisables et les excuter. Le Chapitre 7 (Annulation et arrt) explique comment demander aux tches et aux threads de se terminer avant leur chance normale ; la faon dont les programmes grent lannulation et la terminaison est souvent lun des facteurs permettant de diffrencier les applications concurrentes vraiment robustes de celles qui se contentent de fonctionner. Le Chapitre 8 (Pools de threads) prsente quelques-unes des techniques les plus avances de lexcution des tches. Le Chapitre 9 (Applications graphiques) sintresse aux techniques permettant damliorer le temps de rponse des sous-systmes monothreads. Vivacit, performances et tests. La troisime partie (Chapitres 10 12) soccupe de vrier que les programmes concurrents font bien ce que lon veut quils fassent, tout en ayant des performances acceptables. Le Chapitre 10 (viter les problmes de vivacit) explique comment viter les problmes de vivacit qui peuvent empcher les programmes davancer. Le Chapitre 11 (Performances et adaptabilit) prsente les techniques permettant damliorer les performances et ladaptabilit du code concurrent. Le Chapitre 12 (Tests des programmes concurrents) dcrit les techniques de test du code concurrent, qui permettent de vrier quil est la fois correct et performant. Sujets avancs. La quatrime et dernire partie (Chapitres 13 16) prsente des sujets qui nintresseront probablement que les dveloppeurs expriments : les verrous explicites, les variables atomiques, les algorithmes non bloquants et le dveloppement de synchronisateurs personnaliss.

Prsentation de louvrage

XXV

Exemples de code
Bien que de nombreux concepts gnraux exposs dans ce livre sappliquent aux versions de Java antrieures Java 5.0, voire aux environnements non Java, la plupart des exemples de code (et tout ce qui concerne le modle mmoire de Java) supposent que vous utilisiez Java 5.0 ou une version plus rcente. En outre, certains exemples utilisent des fonctionnalits qui ne sont apparues qu partir de Java 6. Les exemples de code ont t rsums an de rduire leur taille et de mettre en vidence les parties importantes. Leurs versions compltes, ainsi que dautres exemples, sont disponibles sur le site web www.pearson.fr, la page consacre ce livre. Ces exemples sont classs en trois catgories : les "bons", les "moyens" et les "mauvais". Les bons exemples illustrent les techniques conseilles. Les mauvais sont les exemples quil ne faut surtout pas suivre et sont signals par licne "Mr. Yuk" (cette icne est une marque dpose de lhpital des enfants de Pittsburgh, qui nous a autoriss lutiliser), qui indique quil sagit dun code "toxique", comme dans le Listing 1. Les exemples "moyens" illustrent des techniques qui ne sont pas ncessairement mauvaises mais qui sont fragiles ou peu efcaces ; ils sont signals par licne "Peut mieux faire", comme dans le Listing 2.

Listing 1 : Mauvaise faon de trier une liste. Ne le faites pas.


public <T extends Comparable<? super T>> void sort(List<T> list) { // Ne renvoie jamais la mauvaise rponse ! System.exit(0); }

Vous pourriez vous interroger sur lintrt de donner de "mauvais" exemples car, aprs tout, un livre ne devrait expliquer que les bonnes mthodes, pas les mauvaises. Cependant, ces exemples ont deux buts : ils illustrent les piges classiques et, ce qui est le plus important, ils montrent comment faire pour vrier quun programme est thread-safe et la meilleure mthode consiste prsenter les situations o ce nest pas le cas.

Listing 2 : Mthode peu optimale de trier une liste.


public <T extends Comparable<? super T>> void sort(List<T> list) { for (int i=0; i<1000000; i++) neRienFaire(); Collections.sort(list); }

XXVI Programmation concurrente en Java

Remerciements
Ce livre est issu du dveloppement du paquetage java.util.concurrent, qui a t cr par le JSR 166 pour tre inclus dans Java 5.0. De nombreuses personnes ont contribu ce JSR ; nous remercions tout particulirement Martin Buchholz pour le travail quil a effectu an dintgrer le code au JDK, ainsi que tous les lecteurs de la liste de diffusion concurrency-interest, qui ont mis des suggestions sur la proposition initiale des API. Cet ouvrage a t considrablement amlior par les suggestions et laide dune petite arme de relecteurs, de conseillers, de majorettes et de critiques en fauteuil. Nous voudrions remercier Dion Almaer, Tracy Bialik, Cindy Bloch, Martin Buchholz, Paul Christmann, Cliff Click, Stuart Halloway, David Hovemeyer, Jason Hunter, Michael Hunter, Jeremy Hylton, Heinz Kabutz, Robert Kuhar, Ramnivas Laddad, Jared Levy, Nicole Lewis, Victor Luchangco, Jeremy Manson, Paul Martin, Berna Massingill, Michael Maurer, Ted Neward, Kirk Pepperdine, Bill Pugh, Sam Pullara, Russ Rufer, Bill Scherer, Jeffrey Siegal, Bruce Tate, Gil Tene, Paul Tyma et les membres du Silicon Valley Patterns Group, qui, par leurs nombreuses conversations techniques intressantes, ont contribu amliorer ce livre. Nous remercions tout spcialement Cliff Bife, Barry Hayes, Dawid Kurzyniec, Angelika Langer, Doron Rajwan et Bill Venners, qui ont relu lensemble du manuscrit en dtail, trouv des bogues dans les exemples de code et suggr de nombreuses amliorations. Merci Katrina Avery pour son travail ddition et Rosemary Simpson, qui a produit lindex alors que les dlais impartis ntaient pas raisonnables. Merci galement Ami Dewar pour ses illustrations. Nous voulons aussi remercier toute lquipe dAddison-Wesley, qui nous a aids faire de ce livre une ralit. Ann Sellers a lanc le projet et Greg Doench la men jusqu son terme ; Elizabeth Ryan la guid travers tout le processus de production. Merci galement aux milliers de dveloppeurs qui ont contribu indirectement lexistence des logiciels utiliss pour crer ce livre : TeX, LaTeX, Adobe Acrobat, pic, grap, Adobe Illustrator, Perl, Apache Ant, IntelliJIDEA, GNU emacs, Subversion, TortoiseSVN et, bien sr, la plate-forme Java et les bibliothques de classes.

1
Introduction
Si lcriture de programmes corrects est un exercice difcile, lcriture de programmes concurrents corrects lest encore plus. En effet, par rapport un programme squentiel, beaucoup plus de choses peuvent mal tourner dans un programme concurrent. Pourquoi nous intressons-nous alors la concurrence des programmes ? Les threads sont une fonctionnalit incontournable du langage Java et permettent de simplier le dveloppement de systmes complexes en transformant du code asynchrone compliqu en un code plus court et plus simple. En outre, les threads sont le moyen le plus direct dexploiter la puissance des systmes multiprocesseurs. mesure que le nombre de processeurs augmentera, lexploitation de la concurrence prendra de plus en plus dimportance.

1.1

Bref historique de la programmation concurrente

Aux premiers temps de linformatique, les ordinateurs navaient pas de systme dexploitation ; ils excutaient du dbut la n un unique programme qui avait directement accs toutes les ressources de la machine. Non seulement lcriture de ces programmes tait difcile mais lexcution dun seul programme la fois tait un gchis en termes de ressources, qui taient, lpoque, rares et chres. Les systmes dexploitation ont ensuite volu pour permettre plusieurs programmes de sexcuter en mme temps, dans des processus diffrents. Un processus peut tre considr comme une excution indpendante dun programme laquelle le systme dexploitation alloue des ressources comme de la mmoire, des descripteurs de chiers et des droits daccs. Au besoin, les processus peuvent communiquer les uns avec les autres via plusieurs mthodes de communication assez grossires : sockets, signaux, mmoire partage, smaphores et chiers. Plusieurs facteurs dterminants ont conduit au dveloppement des systmes dexploitation, permettant plusieurs programmes de sexcuter simultanment :

Programmation concurrente en Java

Utilisation des ressources. Les programmes doivent parfois attendre des vnements externes, comme une entre ou une sortie de donnes. Un programme qui attend ne faisant rien dutile, il est plus efcace dutiliser ce temps pour permettre un autre programme de sexcuter. quit. Les diffrents utilisateurs et programmes pouvant prtendre aux mmes droits sur les ressources de la machine, il est prfrable de leur permettre de partager cet ordinateur en tranches de temps sufsamment nes, plutt que laisser un seul programme sexcuter jusqu son terme avant den lancer un autre. Commodit. Il est souvent plus simple et plus judicieux dcrire plusieurs programmes effectuant chacun une tche unique et de les combiner ensemble en fonction des besoins, plutt qucrire un seul programme qui ralisera toutes les tches.

Dans les premiers systmes temps partag, chaque processus tait une machine Von Neumann virtuelle : il possdait un espace mmoire pour y stocker la fois ses instructions et ses donnes, il excutait squentiellement les instructions en fonction de la smantique du langage machine et il interagissait avec le monde extrieur via le systme dexploitation au moyen dun ensemble de primitives dentres/sorties. Pour chaque instruction excute, il existait une "instruction suivante" clairement dnie et le programme se droulait selon les rgles du jeu dinstructions. Quasiment tous les langages de programmation actuels suivent encore ce modle squentiel, dans lequel la spcication du langage dnit clairement "ce qui vient aprs" lexcution dune certaine action. Ce modle de programmation squentiel est intuitif et naturel car il modlise lactivit humaine : on effectue une tche la fois, lune la suite de lautre le plus souvent. On se rveille le matin, on enle une robe de chambre, on descend les escaliers et on prpare le caf. Comme dans les langages de programmation, chacune de ces actions du monde rel est une abstraction dune suite dactions plus dtailles on prend un ltre, on dose le caf, on vrie quil y a sufsamment deau dans la cafetire, sil ny en a pas, on la remplit, on allume la cafetire, on attend que leau chauffe, etc. La dernire tape attendre que leau chauffe implique galement un vnement asynchrone. Pendant que leau chauffe, on a le choix attendre devant la cafetire ou effectuer une autre tche comme faire griller du pain (une autre tche asynchrone) ou lire le journal tout en sachant quil faudra bientt soccuper de la cafetire. Les fabricants de cafetires et de grille-pain, sachant que leurs produits sont souvent utiliss de faon asynchrone, ont fait en sorte que ces appareils mettent un signal lorsquils ont effectu leur tche. Trouver le bon quilibre entre squentialit et asynchronisme est souvent la marque des personnes efcaces cest la mme chose pour les programmes. Les raisons (utilisation des ressources, quit et commodit) qui ont motiv le dveloppement des processus ont galement motiv celui des threads. Les threads permettent plusieurs ux du droulement dun programme de coexister dans le mme processus. Bien quils partagent les ressources globales de ce processus, comme la mmoire et les

Chapitre 1

Introduction

descripteurs de chiers, chaque thread possde son propre compteur de programme, sa propre pile et ses propres variables locales. Les threads offrent galement une dcomposition naturelle pour exploiter le paralllisme matriel sur les systmes multiprocesseurs car les diffrents threads dun mme programme peuvent sexcuter simultanment sur des processeurs diffrents. Les threads sont parfois appels processus lgers et les systmes dexploitation modernes considrent les threads, et non les processus, comme units de base pour laccs au processeur. En labsence de coordination explicite, les threads sexcutent en mme temps et de faon asynchrone les uns par rapport aux autres. Comme ils partagent le mme espace dadressage, tous les threads dun processus ont accs aux mmes variables et allouent des objets sur le mme tas : cela leur permet de partager les donnes de faon plus subtile quavec les mcanismes de communication interprocessus. Cependant, en labsence dune synchronisation explicite pour arbitrer laccs ces donnes partages, un thread peut modier des variables quun autre thread est justement en train dutiliser, ce qui aura un effet imprvisible.

1.2

Avantages des threads

Utiliss correctement, les threads permettent de rduire les cots de dveloppement et de maintenance tout en amliorant les performances des applications complexes. Ils facilitent la modlisation du fonctionnement et des changes humains en transformant les tches asynchrones en oprations qui seront gnralement squentielles. Grce eux, un code compliqu peut devenir un code clair, facile crire, relire et maintenir. Dans les applications graphiques, les threads permettent damliorer la ractivit de linterface utilisateur, tandis que, dans les applications serveur, ils optimisent lutilisation des ressources et la rapidit des rponses. Ils simplient galement limplmentation de la machine virtuelle Java (JVM) le ramasse-miettes sexcute gnralement dans un ou plusieurs threads qui lui sont ddis. La plupart des applications Java un tant soit peu complexes utilisent des threads. 1.2.1 Exploitation de plusieurs processeurs

Auparavant, les systmes multiprocesseurs taient rares et chers et ntaient rservs quaux gros centres de calculs et aux traitements scientiques. Aujourdhui, leur prix a considrablement baiss et on en trouve partout, mme sur les serveurs bas de gamme et les stations de travail ordinaires. Cette tendance ne pourra que sacclrer : comme il devient difcile daugmenter les frquences dhorloge, les fabricants prfrent placer plus de processeurs sur le mme circuit. Tous les constructeurs de microprocesseurs se sont lancs dans cette voie et nous commenons voir apparatre des machines dotes dun nombre sans cesse croissant de processeurs.

Programmation concurrente en Java

Le thread tant lunit dallocation du processeur, un programme monothread ne peut sexcuter que sur un seul processeur la fois. Sur un systme deux processeurs, un tel programme perd donc la moiti des ressources CPU disponibles ; sur un systme cent processeurs, il en perdrait 99 %. Les programmes multithreads, en revanche, peuvent sexcuter en parallle sur plusieurs processeurs. Sils sont correctement conus, ces programmes peuvent donc amliorer leurs performances en utilisant plus efcacement les ressources disponibles. Lutilisation de plusieurs threads permet galement damliorer le rendement dun programme, mme sur un systme monoprocesseur. Avec un programme monothread, le processeur restera inactif pendant les oprations dE/S synchrones ; avec un programme multithread, en revanche, un autre thread peut sexcuter pendant que le premier attend la n de lopration dE/S, permettant ainsi lapplication de continuer progresser pendant le blocage d aux E/S (cest comme lire le journal en attendant que leau du caf chauffe, au lieu dattendre que cette eau soit chaude pour lire le journal). 1.2.2 Simplicit de la modlisation

Il est souvent plus simple de grer son temps lorsque lon na quun seul type de tche effectuer (corriger une dizaine de bogues, par exemple) que quand on en a plusieurs (corriger les bogues, interroger les candidats au poste dadministrateur systme, nir le rapport dvaluation de notre quipe et crer les transparents pour la prsentation de la semaine prochaine). Lorsque lon ne doit raliser quun seul type de tche, on peut commencer par le sommet de la pile et travailler jusqu ce que la pile soit vide ; il nest pas ncessaire de rchir ce quil faudra faire ensuite. En revanche, la gestion de priorits et de dates dchance diffrentes et le basculement dune tche vers une autre exigent gnralement un travail supplmentaire. Il en va de mme pour les logiciels : un programme neffectuant squentiellement quun seul type de tche est plus simple crire, moins sujet aux erreurs et plus facile tester quun autre qui gre en mme temps diffrents types de traitements. En affectant un thread chaque type de tche ou chaque lment dune simulation, on obtient lillusion de la squentialit et on isole les traitements des dtails de la planication, des oprations entrelaces, des E/S asynchrones et des attentes de ressources. Un ux dexcution compliqu peut alors tre dcompos en un certain nombre de ux synchrones plus simples, sexcutant chacun dans un thread distinct et ninteragissant avec les autres quen certains points de synchronisation prcis. Cet avantage est souvent exploit par des frameworks comme les servlets ou RMI (Remote Method Invocation). Ceux-ci grent la gestion des requtes, la cration des threads et lquilibre de la charge en rpartissant les parties du traitement des requtes vers le composant appropri un point adquat du ux. Les programmeurs qui crivent les servlets nont pas besoin de soccuper du nombre de requtes qui seront traites simultanment et nont pas savoir si les ux dentre ou de sortie seront bloquants :

Chapitre 1

Introduction

lorsquune mthode dune servlet est appele pour rpondre une requte web, elle peut traiter cette requte de faon synchrone, comme si elle tait un programme monothread. Cela permet de simplier le dveloppement des composants et de rduire le temps dapprentissage de ces frameworks. 1.2.3 Gestion simplie des vnements asynchrones

Une application serveur qui accepte des connexions de plusieurs clients distants peut tre plus simple dvelopper lorsque chaque connexion est gre par un thread qui lui est ddi et quelle peut utiliser des E/S synchrones. Lorsquune application lit une socket qui ne contient aucune donne, la lecture se bloque jusqu ce que des donnes arrivent. Dans une application monothread, cela signie que non seulement le traitement de la requte correspondante se ge, mais que celui de toutes les autres est galement bloqu. Pour viter ce problme, les applications serveur monothreads sont obliges dutiliser des oprations dE/S non bloquantes, ce qui est bien plus compliqu et plus sujet aux erreurs que les oprations dE/S synchrones. Si chaque requte possde son propre thread, en revanche, le blocage naffecte pas le traitement des autres requtes. Historiquement, les systmes dexploitation imposaient des limites assez basses au nombre de threads quun processus pouvait crer : de lordre de quelques centaines (voire moins). Pour compenser cette faiblesse, ces systmes ont donc mis au point des outils efcaces pour grer des E/S multiplexes les appels select et poll dUnix, par exemple. Pour accder ces outils, les bibliothques de classes Java se sont vues dotes dun ensemble de paquetages (java.nio) leur permettant de grer les E/S non bloquantes. Cependant, certains systmes dexploitation acceptent dsormais un nombre bien plus grand de threads, ce qui rend le modle "un thread par client" utilisable, mme pour un grand nombre de clients1. 1.2.4 Interfaces utilisateur plus ractives

Auparavant, les applications graphiques taient monothreads, ce qui impliquait soit dinterroger frquemment le code qui grait les vnements dentre (ce qui est pnible et indiscret), soit dexcuter indirectement tout le code de lapplication dans une "boucle principale de gestion des vnements". Si le code appel partir de cette boucle met trop de temps se terminer, linterface utilisateur semble "se ger" jusqu ce que le code ait ni de sexcuter, car les autres vnements de linterface ne peuvent pas tre traits tant que le contrle nest pas revenu dans la boucle principale. Les frameworks
1. Le paquetage des threads NPTL, qui est maintenant intgr la plupart des distributions Linux, a t conu pour grer des centaines de milliers de threads. Les E/S non bloquantes ont leurs avantages, mais une meilleure gestion des threads par le systme signie que les situations o elles seront ncessaires deviennent plus rares.

Programmation concurrente en Java

graphiques modernes, comme les toolkits AWT ou Swing, remplacent cette boucle par un thread de rpartition des vnements (EDT, event dispatch thread). Lorsquun vnement utilisateur comme lappui dun bouton survient, un thread des vnements appelle les gestionnaires dvnements dnis par lapplication. La plupart des frameworks graphiques tant des sous-systmes monothreads, la boucle des vnements est toujours prsente, mais elle sexcute dans son propre thread, sous le contrle du toolkit, plutt que dans lapplication. Si le thread des vnements nexcute que des tches courtes, linterface reste ractive puisque ce thread est toujours en mesure de traiter assez rapidement les actions de lutilisateur. En revanche, sil contient une tche qui dure un certain temps, une vrication orthographique dun document ou la rcupration dune ressource sur Internet, par exemple, la ractivit de linterface sen ressentira : si lutilisateur effectue une action pendant que cette tche sexcute, il se passera un temps assez long avant que le thread des vnements puisse la traiter, voire simplement en accuser rception. Pour corser le tout, non seulement linterface ne rpondra plus mais il sera impossible dannuler la tche qui pose problme, mme sil y a un bouton "Annuler", puisque le thread des vnements est occup et ne pourra pas traiter lvnement associ ce bouton tant quil na pas termin la tche interminable ! Si, en revanche, ce long traitement sexcute dans un thread spar, le thread des vnements reste disponible pour traiter les actions de lutilisateur, ce qui rend linterface plus ractive.

1.3

Risques des threads

Le support des threads intgr Java est une pe double tranchant. Bien quil simplie le dveloppement des applications concurrentes en fournissant tout ce quil faut au niveau du langage et des bibliothques, ainsi quun modle mmoire formel et portable (cest ce modle formel qui rend possible le dveloppement des applications concurrentes "write-once, run-anywhere" en Java), il place galement la barre un peu plus haut pour les dveloppeurs en les incitant utiliser des threads. Lorsque les threads taient plus sotriques, la programmation concurrente tait un sujet "avanc" ; dsormais, tout bon dveloppeur doit connatre les problmes lis aux threads. 1.3.1 Risques concernant la "thread safety"

Ce type de problme peut tre tonnamment subtil car, en labsence dune synchronisation adapte, lordre des oprations entre les diffrents threads est imprvisible et parfois surprenant. La classe UnsafeSequence du Listing 1.1, cense produire une suite de valeurs entires uniques, est une illustration simple de leffet inattendu de lentrelacement des actions entre diffrents threads. Elle se comporte correctement dans un environnement monothread.

Chapitre 1

Introduction

Listing 1.1 : Gnrateur de squence non thread-safe.


@NotThreadSafe public class UnsafeSequence { private int value; /** Renvoie une valeur unique. */ public int getNext() { return value++; } }

Figure 1.1
Excution malheureuse de

A B

value

9 value 9

9+1

10 9+1 10

value = 10 value = 10

UnsafeSequence .getNext().

Le problme de UnsafeSequence est quavec un peu de malchance deux threads pourraient appeler getNext() et recevoir la mme valeur. La Figure 1.1 montre comment cette situation peut arriver. Bien que la notation value++ puisse sembler dsigner une seule opration, elle en reprsente en ralit trois : lecture de la variable value, incrmentation de sa valeur et stockage de cette nouvelle valeur dans value. Les oprations des diffrents threads pouvant sentrelacer de faon arbitraire lors de lexcution, deux threads peuvent lire cette variable en mme temps, rcuprer la mme valeur et lincrmenter tous les deux. Le rsultat est que le mme nombre sera donc renvoy par des appels diffrents dans des threads distincts.1
Les diagrammes comme celui de la Figure 1.1 dcrivent les entrelacements possibles des excutions de threads diffrents. Dans ces diagrammes, le temps scoule de la gauche vers la droite et chaque ligne reprsente lactivit dun thread particulier. Ces diagrammes dentrelacement dcrivent gnralement le pire des cas possibles 1 et sont conus pour montrer le danger quil y a de supposer que les choses se passeront dans un ordre particulier.

UnsafeSequence utilise lannotation non standard @NotThreadSafe, que nous utiliserons dans ce livre pour documenter les proprits de concurrence des classes et de leurs membres (nous utiliserons galement @ThreadSafe et @Immutable, dcrites dans lannexe A). Ces annotations sont utiles tous ceux qui manipuleront la classe : les utilisateurs dune classe annote par @ThreadSafe, par exemple, sauront quils peuvent lutiliser en toute scurit dans un environnement multithread, les dveloppeurs sauront quils doivent prserver cette proprit, et les outils danalyse pourront identier les ventuelles erreurs de codage.
1. En fait, comme nous le verrons au Chapitre 3, le pire des cas peut tre encore pire que celui prsent dans ces diagrammes, cause dun rarrangement possible des oprations.

Programmation concurrente en Java

UnsafeSequence illustre un danger classique de la concurrence, appel situation de comptition (race condition). Ici, le fait que getNext() renvoie ou non une valeur unique lorsquelle est appele partir de threads diffrents dpend de lentrelacement des oprations lors de lexcution ce qui nest pas souhaitable.

Les threads partageant le mme espace mmoire et sexcutant simultanment peuvent accder ou modier des variables que dautres threads utilisent peut-tre aussi. Cest trs pratique car cela facilite beaucoup le partage des donnes par rapport dautres mcanismes de communication interthread, mais cela prsente galement un risque non ngligeable : les threads peuvent tre perturbs par des donnes modies de faon inattendue. Permettre plusieurs threads daccder aux mmes variables et de les modier introduit un lment de non-squentialit dans un modle de programmation qui est pourtant squentiel, ce qui peut tre troublant et difcile apprhender. Pour que le comportement dun programme multithread soit prvisible, laccs aux variables partages doit donc tre correctement arbitr an que les threads ninterfrent pas les uns avec les autres. Heureusement, Java fournit des mcanismes de synchronisation permettant de coordonner ces accs. An dviter linteraction malheureuse de la Figure 1.1, nous pouvons corriger Unsafe Sequence en synchronisant getNext(), comme le montre le Listing 1.21. Le fonctionnement de ce mcanisme sera dcrit en dtail aux Chapitres 2 et 3.
Listing 1.2 : Gnrateur de squence thread-safe.
@ThreadSafe public class Sequence { @GuardedBy("this") private int Value; public synchronized int getNext() { return nextValue++; } }

En labsence de synchronisation, le compilateur, le matriel et lexcution peuvent prendre quelques liberts concernant le timing et lordonnancement des actions, comme mettre les variables en cache dans des registres ou des caches locaux du processeur o elles seront temporairement (voire dnitivement) invisibles aux autres threads. Bien que ces astuces permettent dobtenir de meilleures performances et soient gnralement souhaitables, elles obligent le dveloppeur savoir prcisment o se trouvent les donnes qui sont partages entre les threads, an que ces optimisations ne dtriorent pas le comportement (le Chapitre 16 prsentera les dtails croustillants sur lordonnancement garanti par la JVM et sur la faon dont la synchronisation inue sur ces garanties mais, si vous suivez les rgles des Chapitres 2 et 3, vous pouvez vous passer de ces dtails de bas niveau).
1. @GuardedBy est dcrite dans la section 2.4 ; elle documente la politique de synchronisation pour Sequence.

Chapitre 1

Introduction

1.3.2

Risques sur la vivacit

Il est essentiel de veiller ce que le code soit thread-safe lorsque lon dveloppe du code concurrent. Cette "thread safety" est incontournable et nest pas rserve aux programmes multithreads les programmes monothreads doivent galement sen proccuper mais lutilisation des threads introduit des risques supplmentaires qui nexistent pas dans les programmes monothreads. De mme, lutilisation de plusieurs threads introduit des risques supplmentaires sur la vivacit qui nexistent pas lorsquil ny a quun seul thread. Alors que "safety" signie "rien de mauvais ne peut se produire", la vivacit reprsente le but complmentaire, "quelque chose de bon nira par arriver". Une panne de vivacit survient lorsquune activit se trouve dans un tat tel quelle ne peut plus progresser. Une des formes de cette panne pouvant intervenir dans les programmes squentiels est la fameuse boucle sans n, o le code qui suit la boucle ne sera jamais excut. Lutilisation des threads introduit de nouveaux risques pour cette vivacit : si le thread A, par exemple, attend une ressource dtenue de faon exclusive par le thread B et que B ne la libre jamais, A sera bloqu pour toujours. Le Chapitre 10 dcrit les diffrentes formes de pannes de vivacit et explique comment les viter. Parmi ces pannes, citons les interblocages (dreadlocks) (section 10.1), la famine (section 10.3.1) et les livelocks. Comme la plupart des bogues de concurrence, ceux qui provoquent des pannes de vivacit peuvent tre difciles reprer car ils dpendent du timing relatif des vnements dans les diffrents threads et ne se manifestent donc pas toujours pendant les phases de dveloppement et de tests. 1.3.3 Risques sur les performances

Les performances sont lies la vivacit. Alors que cette dernire signie que quelque chose nira par arriver, ce "nira par" peut ne pas sufre on souhaite souvent que les bonnes choses arrivent vite. Les problmes de performance incluent un vaste domaine de problmes, dont les mauvais temps de rponse, une ractivit qui laisse dsirer, une consommation excessive des ressources ou une mauvaise adaptabilit. Comme avec la "safety" et la vivacit, les programmes multithreads sont sujets tous les problmes de performance des programmes monothreads, mais ils souffrent galement de ceux qui sont introduits par lutilisation des threads. Pour les applications concurrentes bien conues, lutilisation des threads apporte un net gain de performance, mais les threads ont galement un certain cot en terme dexcution. Les changements de contexte lorsque lordonnanceur suspend temporairement le thread actif pour quun autre thread puisse sexcuter sont plus frquents dans les applications utilisant de nombreux threads et ont des cots non ngligeables : sauvegarde et restauration du contexte dexcution, perte de localit et temps CPU pass ordonnancer les threads plutt qu les excuter. En outre, lorsque des threads partagent des donnes, ils doivent utiliser des mcanismes de synchronisation qui peuvent empcher le compilateur deffectuer des optimisations, il faut quils vident ou invalident les caches mmoire et

10

Programmation concurrente en Java

quils crent un trac synchronis sur le bus mmoire partag. Tous ces aspects se payent en termes de performance ; le Chapitre 11 prsentera les techniques permettant danalyser et de rduire ces cots.

1.4

Les threads sont partout

Mme si votre programme ne cre jamais explicitement de thread, les frameworks peuvent en crer pour vous et le code appel partir de ces threads doit tre thread-safe. Cet aspect peut reprsenter une charge non ngligeable pour les dveloppeurs lorsquils conoivent et implmentent leurs applications car dvelopper des classes thread-safe ncessite plus dattention et danalyse que dvelopper des classes qui ne le sont pas. Toutes les applications Java utilisent des threads. Lorsque la JVM se lance, elle cre des threads pour ses tches de nettoyage (ramasse-miettes, nalisation) et un thread principal pour excuter la mthode main(). Les frameworks graphiques AWT (Abstract Window Toolkit) et Swing crent des threads pour grer les vnements de linterface utilisateur ; Timer cre des threads pour excuter les tches diffres ; les frameworks composants, comme les servlets et RMI, crent des pools de threads et invoquent les mthodes composant dans ces threads. Si vous utilisez ces outils comme le font de nombreux dveloppeurs , vous devez prendre lhabitude de la concurrence et de la thread safety car ces frameworks crent des threads partir desquels ils appellent vos composants. Il serait agrable de penser que la concurrence est une fonctionnalit "facultative" ou "avance" du langage mais, en ralit, quasiment toutes les applications Java sont multithreads et ces frameworks ne vous dispensent pas de la ncessit de coordonner correctement laccs ltat de lapplication. Lorsquun framework ajoute de la concurrence dans une application, il est gnralement impossible de restreindre la concurrence du code du framework car, de par leur nature, les frameworks crent des fonctions de rappel vers les composants de lapplication, qui, leur tour, accdent ltat de lapplication. De mme, le besoin dun code thread-safe ne se cantonne pas aux composants appels par le framework il stend tout le code qui accde ltat du programme. Ce besoin est donc contagieux.
Les frameworks introduisent la concurrence dans les applications en appelant les composants des applications partir de leurs threads. Les composants accdent invariablement ltat de lapplication, ce qui ncessite donc que tous les chemins du code accdant cet tat soient thread-safe.

Dans tous les outils que nous dcrivons ci-aprs, le code de lapplication sera appel partir de threads qui ne sont pas grs par lapplication. Bien que le besoin de thread safety puisse commencer avec ces outils, il se termine rarement l ; il a plutt tendance se propager dans lapplication.

Chapitre 1

Introduction

11

Timer. Timer est un mcanisme permettant de planier lexcution de tches une date future, soit une seule fois, soit priodiquement. Lintroduction dun Timer peut compliquer un programme squentiel car les TimerTask sexcutent dans un thread gr par le Timer, pas par lapplication. Si un TimerTask accde des donnes galement utilises par dautres threads de lapplication, non seulement le TimerTask doit le faire de faon thread-safe, mais toutes les autres classes qui accdent ces donnes doivent faire de mme. Souvent, le moyen le plus simple consiste sassurer que les objets auxquels accde un TimerTask sont eux-mmes thread-safe, ce qui permet dencapsuler cette thread safety dans les objets partags. Servlets et JavaServer Pages (JSPs). Le framework des servlets a t conu pour prendre en charge toute linfrastructure de dploiement dune application web et pour rpartir les requtes provenant de clients HTTP distants. Une requte qui arrive au serveur est dirige, ventuellement via une chane de ltres, vers la servlet ou la JSP approprie. Chaque servlet reprsente un composant de lapplication ; sur les sites de grande taille, plusieurs clients peuvent demander en mme temps les services de la mme servlet. Dailleurs, la spcication des servlets exige quune servlet puisse tre appele simultanment partir de threads diffrents : en dautres termes, les servlets doivent tre thread-safe. Mme si vous pouviez garantir quune servlet ne sera appele que par un seul thread la fois, vous devriez quand mme vous proccuper de la thread safety lorsque vous construisez une application web. En effet, les servlets accdent souvent des informations partages par dautres servlets, par exemple les objets globaux de lapplication (ceux qui sont stocks dans le ServletContext) ou les objets de la session (ceux qui sont stocks dans le HttpSession de chaque client). Une servlet accdant des objets partags par dautres servlets ou par les requtes doit donc coordonner correctement laccs ces objets puisque plusieurs requtes peuvent y accder simultanment, partir de threads distincts. Les servlets et les JSP, ainsi que les ltres des servlets et les objets stocks dans des conteneurs comme ServletContext et HttpSession, doivent donc tre thread-safe. Appels de mthodes distantes. RMI permet dappeler des mthodes sur des objets qui sexcutent sur une autre JVM. Lorsque lon appelle une mthode distante avec RMI, les paramtres dappel sont empaquets (srialiss) dans un ux doctets qui est envoy via le rseau la JVM distante qui les extrait (dsrialise) avant de les passer la mthode. Lorsque le code RMI appelle lobjet distant, on ne peut pas savoir dans quel thread cet appel aura lieu ; il est clair que cest non pas dans un thread que lon a cr mais dans un thread gr par RMI. Combien de threads cre RMI ? Est-ce que la mme mthode sur le mme objet distant pourrait tre appele simultanment dans plusieurs threads RMI 1 ?
1. La rponse est oui, bien que ce ne soit pas clairement annonc dans la documentation Javadoc. Vous devez lire les spcications de RMI.

12

Programmation concurrente en Java

Un objet distant doit se protger contre deux risques lis aux threads : il doit correctement coordonner laccs ltat partag avec les autres objets et laccs ltat de lobjet distant lui-mme (puisque le mme objet peut tre appel simultanment dans plusieurs threads). Comme les servlets, les objets RMI doivent donc tre prvus pour tre appels simultanment et doivent donc tre thread-safe. Swing et AWT. Par essence, les applications graphiques sont asynchrones car les utilisateurs peuvent slectionner un lment de menu ou presser un bouton nimporte quel moment et sattendre ce que lapplication rponde rapidement, mme si elle est au beau milieu dun traitement. Pour grer ce problme, Swing et AWT crent un thread distinct pour prendre en charge les vnements produits par lutilisateur et mettre jour la vue qui lui sera prsente. Les composants Swing, comme JTable, ne sont pas thread-safe, mais Swing les protge en connant dans le thread des vnements tous les accs ces composants. Si une application veut manipuler linterface graphique depuis lextrieur de ce thread, elle doit faire en sorte que son code sexcute dans ce thread. Lorsque lutilisateur interagit avec linterface, un gestionnaire dvnement est appel pour effectuer lopration demande. Si ce gestionnaire doit accder un tat de lapplication qui est aussi utilis par dautres threads (le document dit, par exemple), le gestionnaire et lautre code qui accde cet tat doivent le faire de faon thread-safe.

I
Les bases

2
Thread safety
Assez tonnamment, la programmation concurrente ne concerne pas beaucoup plus les threads ou les verrous quun ingnieur des travaux publics ne manipule des rivets ou des poutrelles dacier. Cela dit, la construction de ponts qui ne scroulent pas implique videmment une utilisation correcte de trs nombreux rivets et poutrelles, tout comme la construction de programmes concurrents exige une utilisation correcte des threads et des verrous, mais ce sont simplement des mcanismes des moyens darriver ses ns. Essentiellement, lcriture dun code thread-safe consiste grer laccs un tat, notamment un tat partag et modiable. De faon informelle, ltat dun objet est form de ses donnes, qui sont stockes dans des variables dtat comme les attributs dinstance ou de classe. Ltat dun objet peut contenir les attributs dautres objets : ltat dun HashMap, par exemple, est en partie stock dans lobjet lui-mme, mais galement dans les nombreux objets Map.Entry. Ltat dun objet comprend toutes les donnes qui peuvent affecter son comportement extrieur. Partag signie quon peut accder la variable par plusieurs threads ; modiable indique que la valeur de cette variable peut varier au cours de son existence. Bien que nous puissions parler de la thread safety comme si elle concernait le code, ce que nous tentons rellement de raliser consiste protger les donnes contre les accs concurrents indsirables. La ncessit quun objet ait besoin dtre thread-safe dpend du fait quon puisse y accder partir de threads diffrents. Cela dpend donc de lutilisation de lobjet dans un programme, pas de ce quil fait. Crer un objet thread-safe ncessite dutiliser une synchronisation pour coordonner les accs son tat modiable ; ne pas le faire peut perturber les donnes ou impliquer dautres consquences nfastes. chaque fois que plusieurs threads accdent une variable dtat donne et que lun dentre eux est susceptible de la modier, tous ces threads doivent coordonner leurs

16

Les bases

Partie I

accs via une synchronisation. En Java, le mcanisme principal de cette synchronisation est le mot-cl synchronized, qui fournit un verrou exclusif, mais le terme de "synchronisation" comprend galement lutilisation de variables volatile, de verrous explicites et de variables atomiques. Ne faites pas lerreur de penser quil existe des situations "spciales" pour lesquelles cette rgle ne sapplique pas. Un programme qui neffectue pas de synchronisation alors quelle est ncessaire peut sembler fonctionner, passer les tests et se comporter normalement pendant des annes, ce qui ne lempche pas dtre incorrect et de pouvoir chouer tout moment.

Si plusieurs threads accdent la mme variable dtat modiable sans utiliser de synchronisation adquate, le programme est incorrect. Il existe trois moyens de corriger ce problme : ne pas partager la variable dtat entre les threads ; rendre la variable dtat non modiable ; utiliser la synchronisation chaque fois que lon accde la variable dtat.

Si vous navez pas prvu les accs concurrents lors de la conception de votre classe, certaines de ces approches pourront demander des modications substantielles : corriger le problme peut ne pas tre aussi simple quil ny parat. Il est bien plus facile de concevoir ds le dpart une classe qui est thread-safe que de lui ajouter plus tard cette thread safety. Dans un gros programme, il peut tre difcile de savoir si plusieurs threads sont susceptibles daccder une variable donne. Heureusement, les techniques orientes objets qui permettent dcrire des classes bien organises et faciles maintenir comme lencapsulation et labstraction des donnes peuvent galement vous aider crer des classes thread-safe. Plus le code qui doit accder une variable particulire est rduit, plus il est facile de vrier quil utilise une synchronisation approprie et de rchir aux conditions daccs cette variable. Le langage Java ne vous force pas encapsuler ltat il est tout fait possible de stocker cet tat dans des membres publics (voire des membres de classe publics) ou de fournir une rfrence vers un objet interne , mais plus ltat du programme est encapsul, plus il est facile de le rendre thread-safe et daider les dveloppeurs le conserver comme tel.

Lorsque lon conoit des classes thread-safe, les techniques orientes objet encapsulation, imutabilit et spcication claire des invariants sont dune aide inestimable.

Chapitre 2

Thread safety

17

Parfois, les techniques de conception orientes objet ne permettent pas de reprsenter les besoins du monde rel ; dans ce cas, il peut tre ncessaire de trouver un compromis pour des raisons de performance ou de compatibilit ascendante avec le code antrieur. Parfois, labstraction et lencapsulation ne font pas bon mnage avec les performances bien que ce soit plus rare, contrairement ce que pensent de nombreux dveloppeurs , mais il est toujours prfrable dcrire dabord un bon code avant de le rendre rapide. Mme alors, noptimisez le code que si cela est ncessaire et si vos mesures ont montr que cette optimisation fera une diffrence dans des conditions ralistes 1. Si vous dcidez de briser lencapsulation, tout nest quand mme pas perdu. Votre programme pourra quand mme tre thread-safe : ce sera juste beaucoup plus dur. En outre, cette thread safety sera plus fragile en augmentant non seulement le cot et le risque du dveloppement, mais galement le cot et le risque de la maintenance. Le Chapitre 4 prcisera les conditions sous lesquelles on peut relcher sans problme lencapsulation des variables dtat. Jusqu maintenant, nous avons utilis presque indiffremment les termes "classe threadsafe" et "programme thread-safe". Un programme thread-safe est-il un programme qui nest constitu que de classes thread-safe ? Pas ncessairement un programme form uniquement de classes thread-safe peut ne pas ltre lui-mme et un programme threadsafe peut contenir des classes qui ne le sont pas. Les problmes lis la composition des classes thread-safe seront galement prsents au Chapitre 4. Quoi quil en soit, le concept de classe thread-safe na de sens que si la classe encapsule son propre tat. La thread safety peut tre un terme qui sapplique au code, mais il concerne ltat et ne peut sappliquer qu tout un corps de code qui encapsule son tat, que ce soit un objet ou tout un programme.

2.1

Quest-ce que la thread safety ?

Dnir la thread safety est tonnamment difcile. Les tentatives les plus formelles sont si compliques quelles sont peu utiles ou comprhensibles, les autres ne sont que des descriptions informelles qui semblent tourner en rond. Une recherche rapide sur Google produit un grand nombre de "dnitions" comme celles-ci :
m

[] Peut tre appele partir de plusieurs threads du programme sans quil y ait dinteractions indsirables entre les threads. [] Peut tre appele par plusieurs threads simultanment sans ncessiter dautre action de la part de lappelant.

1. Dans du code concurrent, cette pratique est dautant plus souhaitable que les bogues lis la concurrence sont difciles reproduire et dtecter. Le bnce dun petit gain de performance sur certaines parties du code qui ne sont pas souvent utilises peut trs bien tre occult par le risque que le programme choue sur le terrain.

18

Les bases

Partie I

Avec ce genre de dnition, il nest pas tonnant que ce terme soit confus ! Elles ressemblent trangement "une classe est thread-safe si elle peut tre utilise sans problme par plusieurs threads". On ne peut pas vraiment critiquer ce type dafrmation, mais cela ne nous aide pas beaucoup non plus. Que signie "safe", tout dabord ? La notion de "correct" est au cur de toute dnition raisonnable de thread safety. Si notre dnition est oue, cest parce que nous navons pas donn une dnition claire de cette notion. Une classe est correcte si elle se conforme sa spcication, et une bonne spcication dnit les invariants qui contraignent ltat dun objet et les postconditions qui dcrivent les effets de ses oprations. Comme on crit rarement les spcications adquates pour nos classes, comment savoir quelles sont correctes ? Nous ne le pouvons pas, mais cela ne nous empche pas de les utiliser quand mme une fois que nous sommes srs que "le code fonctionne". Pour nombre dentre nous, cette "conance dans le code" se rapproche beaucoup de la notion de correct et nous supposerons simplement quun code monothread correct est quelque chose que "nous croyons quand nous le voyons". Maintenant que nous avons donn une dnition optimiste de "correct", nous pouvons dnir la thread safety de faon un peu moins circulaire : une classe est thread-safe si elle continue de se comporter correctement lorsquon lutilise partir de plusieurs threads.
Une classe est thread-safe si elle se comporte correctement lorsquon lutilise partir de plusieurs threads, quel que soit lordonnancement ou lentrelacement de lexcution de ces threads et sans synchronisation ni autre coordination supplmentaire de la part du code appelant.

Tout programme monothread tant galement un programme multithread valide, il ne peut pas tre thread-safe sil nest mme pas correct dans un environnement monothread 1. Si un objet est correctement implment, aucune squence doprations appels des mthodes publiques et lecture ou criture des champs publics ne devrait pouvoir violer ses invariants ou ses postconditions. Aucun ensemble doprations excutes en squence ou paralllement sur des instances dune classe thread-safe ne peut placer une instance dans un tat invalide.
Les classes thread-safe encapsulent toute la synchronisation ncessaire pour que les clients naient pas besoin de fournir la leur.

1. Si cette utilisation un peu oue de "correct" vous ennuie, vous pouvez considrer quune classe thread-safe est une classe qui nest pas plus incorrecte dans un environnement concurrent que dans un environnement monothread.

Chapitre 2

Thread safety

19

2.1.1

Exemple : une servlet sans tat

Au Chapitre 1, nous avons numr un certain nombre de frameworks qui crent des threads et appellent vos composants partir de ceux-ci en vous laissant la responsabilit de crer des composants thread-safe. Trs souvent, on doit crer des classes thread-safe, non parce que lon souhaite utiliser directement des threads, mais parce que lon veut bncier dun framework comme celui des servlets. Nous allons dvelopper un exemple simple un service de factorisation reposant sur une servlet que nous tendrons petit petit tout en prservant sa thread safety. Le Listing 2.1 montre le code de cette servlet. Elle extrait de la requte le nombre factoriser, le met en facteur et ajoute le rsultat la rponse.
Listing 2.1 : Une servlet sans tat.
@ThreadSafe public class StatelessFactorizer implements Servlet { public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); encodeIntoResponse(resp, factors); } }

StatelessFactorizer, comme le plupart des servlets, est sans tat : elle ne possde aucun champ et ne fait rfrence aucun champ dautres classes. Ltat transitoire pour un calcul particulier nexiste que dans les variables locales stockes sur la pile du thread, qui ne sont accessibles que par le thread qui sexcute. Un thread accdant une StatelessFactorizer ne peut pas inuer sur le rsultat dun autre thread accdant cette mme StatelessFactorizer : les deux threads ne partagent pas dtat, comme sils accdaient des instances diffrentes. Les actions dun thread accdant un objet sans tat ne pouvant rendre incorrectes les oprations dans les autres threads, les objets sans tat sont thread-safe.
Les objets sans tat sont toujours thread-safe.

Le fait que la plupart des servlets puissent tre implmentes sans tat rduit beaucoup le souci den faire des servlets thread-safe. Ce nest que lorsque les servlets veulent mmoriser des informations dune requte lautre que cette thread safety devient un problme.

2.2

Atomicit

Que se passe-t-il lorsquon ajoute une information dtat un objet sans tat ? Supposons par exemple que nous voulions ajouter un "compteur de visites" pour comptabiliser le nombre de requtes traites par notre servlet. Une approche vidente consiste ajouter

20

Les bases

Partie I

un champ de type long la servlet et de lincrmenter chaque requte, comme on le fait dans la classe UnsafeCountingFactorizer du Listing 2.2.
Listing 2.2 : Servlet comptant le nombre de requtes sans la synchronisation ncessaire. Ne le faites pas.
@NotThreadSafe public class UnsafeCountingFactorizer implements Servlet { private long count = 0; public long getCount() { return count; } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); ++count; encodeIntoResponse(resp, factors); } }

Malheureusement, UnsafeCountingFactorizer nest pas thread-safe, mme si elle fonctionnerait parfaitement dans un environnement monothread. En effet, comme Unsafe Sequence du Chapitre 1, cette classe est susceptible de perdre des mises jour. Bien que lopration dincrmentation ++count puisse sembler tre une action simple cause de sa syntaxe compacte, elle nest pas atomique, ce qui signie quelle ne sexcute pas comme une unique opration indivisible. Cest, au contraire, un raccourci dcriture pour une suite de trois oprations : obtention de la valeur courante, ajout de un cette valeur et stockage de la nouvelle valeur la place de lancienne. Cest donc un exemple dopration lire-modier-crire dans laquelle ltat nal dpend de ltat prcdent. La Figure 1.1 du Chapitre 1 a montr ce qui pouvait se passer lorsque deux threads essayaient dincrmenter un compteur simultanment. Si ce compteur vaut initialement 9, un timing malheureux pourrait faire que les deux threads lisent la variable, constatent quelle vaut 9, lui ajoutent un et xent donc tous les deux le compteur 10, ce qui nest certainement pas ce que lon attend. On a perdu une incrmentation, et le compteur de visites vaut maintenant un de moins que ce quil devrait valoir. Vous pourriez penser que, pour un service web, on peut se satisfaire dun compteur de visites lgrement imprcis, et cest effectivement parfois le cas. Mais, si ce compteur sert produire des squences didentiants uniques pour des objets et quil renvoie la mme valeur en rponse plusieurs appels, cela risque de poser de srieux problmes dintgrit des donnes1. En programmation concurrente, la possibilit dobtenir des rsultats incorrects cause dun timing malheureux est si importante quon lui a donn un nom : situation de comptition (race condition).

1. Lapproche utilise par UnsafeSequence et UnsafeCountingFactorizer souffre dautres problmes importants, dont la possibilit davoir des donnes obsoltes (voir la section 3.1.1).

Chapitre 2

Thread safety

21

2.2.1

Situations de comptition

UnsafeCountingFactorizer a plusieurs situations de comptition qui rendent ses rsultats non ables. Une situation de comptition apparat lorsque lexactitude dun calcul dpend de lordonnancement ou de lentrelacement des diffrents threads lors de lexcution ; en dautres termes lorsque lobtention dune rponse correcte dpend de la chance 1. La situation de comptition la plus frquente est vrier-puis-agir, o une observation potentiellement obsolte sert prendre une dcision sur ce quil faut faire ensuite.

Nous rencontrons souvent des situations de comptition dans la vie de tous les jours. Supposons que vous ayez prvu de rencontrer un ami aprs midi dans un caf de lavenue Crampel. Arriv l, vous vous rendez compte quil y a deux cafs dans cette avenue et vous ntes pas sr de celui o vous vous tes donn rendez-vous. midi dix, votre ami nest pas dans le caf A et vous allez donc dans le caf B pour voir sil sy trouve, or il ny est pas non plus. Il reste alors peu de possibilits : votre ami est en retard et nest dans aucun des cafs ; votre ami est arriv au caf A aprs que vous lavez quitt ou votre ami tait dans le caf B, vous cherche et est maintenant en route vers le caf A. Supposons le pire des scnarios, qui est la dernire de ces trois solutions. Il est maintenant midi quinze, vous tes alls tous les deux dans les deux cafs et vous vous demandez tous les deux si vous vous tes manqus. Que faire maintenant ? Retourner dans lautre caf ? Combien de fois allez-vous aller et venir ? moins de vous tre mis daccord sur un protocole, vous risquez de passer la journe parcourir lavenue Crampel, aigri et en manque de cafine. Le problme avec lapproche "je vais juste descendre la rue et voir sil est dans lautre caf" est que, pendant que vous marchez dans la rue, votre ami peut stre dplac. Vous regardez dans le caf A, constatez qu"il nest pas l" et vous continuez le chercher. Vous pourriez faire de mme avec le caf B, mais pas en mme temps. Il faut quelques minutes pour aller dun caf lautre et, pendant ce temps, ltat du systme peut avoir chang. Cet exemple des cafs illustre une situation de comptition puisque lobtention du rsultat voulu (rencontrer votre ami) dpend du timing relatif des vnements (linstant o chacun de vous arrive dans lun ou lautre caf, le temps dattente avant de partir dans lautre caf, etc.). Lobservation que votre ami nest pas dans le caf A devient
1. Le terme race condition est souvent confondu avec celui de data race, qui intervient lorsque lon nutilise pas de synchronisation pour coordonner tous les accs un champ partag non constant. chaque fois quun thread crit dans une variable qui pourrait ensuite tre lue par un autre thread ou lit une variable qui pourrait avoir t crite par un autre thread, on risque un data race si les deux threads ne sont pas synchroniss. Un code contenant des data races na aucune smantique dnie dans le modle mmoire de Java. Toutes les race conditions ne sont pas des data races et toutes les data races ne sont pas des race conditions, mais toutes les deux font chouer les programmes concurrents de faon non prvisible. UnsafeCountingFactorizer contient la fois des race conditions et des data races. Voir le Chapitre 16 pour plus dinformations sur les data races.

22

Les bases

Partie I

potentiellement obsolte ds que vous en ressortez puisquil pourrait y tre entr par la porte de derrire sans que vous le sachiez. Cest cette obsolescence des observations qui caractrise la plupart des situations de comptition lutilisation dune observation potentiellement obsolte pour prendre une dcision ou effectuer un calcul. Ce type de situation de comptition sappelle tester-puis-agir : vous observez que quelque chose est vrai (le chier X nexiste pas), puis vous prenez une dcision en fonction de cette observation (crer X) mais, en fait, lobservation a pu devenir obsolte entre le moment o vous lavez observe et celui o vous avez agi (quelquun dautre a pu crer X entretemps), ce qui pose un problme (une exception inattendue, des donnes crases, un chier abm). 2.2.2 Exemple : situations de comptition dans une initialisation paresseuse

Linitialisation paresseuse est un idiome classique de lutilisation de tester-puis-agir. Le but dune initialisation paresseuse consiste diffrer linitialisation dun objet jusqu ce que lon en ait rellement besoin tout en garantissant quil ne sera initialis quune seule fois. La classe LazyInitRace du Listing 2.3 illustre cet idiome. La mthode getInstance() commence par tester si lobjet ExpensiveObject a dj t initialis, auquel cas elle renvoie linstance existante ; sinon elle cre une nouvelle instance quelle renvoie aprs avoir mmoris sa rfrence pour que les futurs appels naient pas reproduire ce code coteux.
Listing 2.3 : Situation de comptition dans une initialisation paresseuse. Ne le faites pas.
@NotThreadSafe public class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance() { if (instance == null) instance = new ExpensiveObject(); return instance; } }

LazyInitRace contient des situations de comptition qui peuvent perturber son fonctionnement. Supposons par exemple que les threads A et B excutent getInstance(). A constate que instance vaut null et instancie un nouvel objet ExpensiveObject. B teste galement instance, or le rsultat de ce test dpend du timing, qui nest pas prvisible puisquil est fonction des caprices de lordonnancement et du temps que met A pour instancier ExpensiveObject et initialiser le champ instance. Si celui-ci vaut null lorsque B le teste, les deux appels getInstance() peuvent produire deux rsultats diffrents, bien que cette mthode soit cense toujours renvoyer la mme instance.

Le comptage des visites dans UnsafeCountingFactorizer contient une autre sorte de situation de comptition. Les oprations lire-modier-crire, ce qui est le cas de lincrmentation dun compteur, dnissent une transformation de ltat dun objet partir de

Chapitre 2

Thread safety

23

son tat antrieur. Pour incrmenter un compteur, il faut connatre sa valeur prcdente et sassurer que personne dautre ne modie ou nutilise cette valeur pendant que lon est en train de la modier. Comme la plupart des problmes de concurrence, les situations de comptition ne provoquent pas toujours de panne : pour cela, il faut galement un mauvais timing. Cependant, les situations de comptition peuvent poser de srieux problmes. Si LazyInitRace est utilise pour instancier un enregistrement de compte, par exemple, le fait quelle ne renvoie pas la mme instance lorsquon lappelle plusieurs fois pourrait provoquer la perte dinscriptions ou les diffrentes activits pourraient avoir des vues incohrentes de lensemble des inscrits. Si UnsafeSequence est utilise pour produire des identiants uniques, deux objets distincts pourraient recevoir le mme identiant, violant ainsi les contraintes dintgrit concernant lidentit des objets. 2.2.3 Actions composes

LazyInitRace et UnsafeCountingFactorizer contenaient toutes les deux une squence doprations qui aurait d tre atomique, cest--dire indivisible, par rapport aux autres oprations sur le mme tat. Pour viter les situations de comptition, on doit disposer dun moyen dempcher dautres threads dutiliser une variable que lon est en train de modier an de pouvoir garantir que ces threads ne pourront observer ou modier ltat quavant ou aprs, mais pas en mme temps que nous.
Les oprations A et B sont atomiques lune pour lautre si, du point de vue du thread qui excute A, lorsquun autre thread excute B, lopration est excute dans son intgralit ou pas du tout. Une opration atomique lest donc par rapport toutes les oprations, y compris elle-mme, qui manipulent le mme tat.

Si lopration dincrmentation de UnsafeSequence avait t atomique, la situation de comptition illustre par la Figure 1.1 naurait pas pu avoir lieu et chaque excution de cette opration aurait incrment le compteur de un exactement, comme on lattendait. Pour garantir la thread safety, les oprations tester-puis-agir (comme linitialisation paresseuse) et lire-modier-crire (comme lincrmentation) doivent toujours tre atomiques. Ces deux types doprations sont des actions composes, cest--dire des squences doprations qui doivent tre excutes de faon atomique an de rester thread-safe. Dans la section suivante, nous tudierons les verrous, un mcanisme intgr Java permettant de garantir latomicit mais, pour linstant, nous allons rsoudre notre problme dune autre faon en utilisant une classe thread-safe existante, comme dans le Listing 2.4.

24

Les bases

Partie I

Listing 2.4 : Servlet comptant les requtes avec AtomicLong.


@ThreadSafe public class CountingFactorizer implements Servlet { private final AtomicLong count = new AtomicLong(0); public long getCount() { return count.get(); } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); count.incrementAndGet(); encodeIntoResponse(resp, factors); } }

Le paquetage java.util.concurrent.atomic contient des classes de variables atomiques permettant deffectuer des changements dtat atomiques sur les nombres et les rfrences dobjets. En remplaant le type long du compteur par AtomicLong, nous garantissons donc que tous les accs ltat du compteur seront atomiques1. Ltat de la servlet tant ltat du compteur qui est dsormais thread-safe, notre servlet le devient galement. Nous avons pu ajouter un compteur notre servlet de factorisation et maintenir la thread safety en utilisant une classe thread-safe existante, AtomicLong, pour grer ltat du compteur. Lorsque lon ajoute un unique lment dtat une classe sans tat, cette classe sera thread-safe si ltat est entirement gr par un objet thread-safe. Cependant, comme nous le verrons dans la prochaine section, passer dune seule variable dtat plusieurs nest pas forcment aussi simple que passer de zro un.
chaque fois que cela est possible, utilisez des objets thread-safe existants, comme AtomicLong, pour grer ltat de votre classe. Il est en effet plus facile de rchir aux tats possibles et aux transitions dtat des objets thread-safe existants que de le faire pour des variables dtat quelconques ; en outre, cela facilite la maintenance et la vrication de la thread safety.

2.3

Verrous

Nous avons pu ajouter une variable dtat notre servlet tout en maintenant la thread safety car nous avons utilis un objet thread-safe pour grer tout ltat de la servlet. Mais que se passe-t-il si lon souhaite ajouter dautres lments dtat ? Peut-on se contenter dajouter dautres variables dtat thread-safe ? Imaginons que nous voulions amliorer les performances de notre servlet en mettant en cache le dernier rsultat calcul, juste au cas o deux requtes conscutives demanderaient factoriser le mme nombre (ce nest srement pas une bonne stratgie de cache ; nous en prsenterons une meilleure
1. Pour incrmenter le compteur, CountingFactorizer appelle incrementAndGet(), qui renvoie galement la valeur incrmente. Ici, cette valeur est ignore.

Chapitre 2

Thread safety

25

dans la section 5.6). Pour implmenter ce cache, nous devons mmoriser la fois le nombre factoris et ses facteurs. Comme nous avons utilis la classe AtomicLong pour grer ltat du compteur de faon thread-safe, nous pourrions peut-tre utiliser sa cousine, AtomicReference1, pour grer le dernier nombre et ses facteurs. La classe UnsafeCachingFactorizer du Listing 2.5 implmente cette tentative.
Listing 2.5 : Servlet tentant de mettre en cache son dernier rsultat sans latomicit adquate. Ne le faites pas.
@NotThreadSafe public class UnsafeCachingFactorizer implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber.get())) encodeIntoResponse(resp, lastFactors.get()); else { BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } } }

Malheureusement, cette approche ne fonctionne pas. Bien que, individuellement, les rfrences atomiques soient thread-safe, UnsafeCachingFactorizer contient des situations de comptition qui peuvent lui faire produire une mauvaise rponse. La dnition de thread-safe implique que les invariants soient prservs quel que soit le timing ou lentrelacement des oprations entre les threads. Un des invariants de Unsafe CachingFactorizer est que le produit des facteurs mis en cache dans lastFactors est gal la valeur mise en cache dans lastNumber ; la servlet ne sera correcte que si cet invariant est toujours respect. Lorsquun invariant implique plusieurs variables, celles-ci ne sont pas indpendantes : la valeur de lune contraint la ou les valeurs des autres. Par consquent, lorsquon modie lune de ces variables, il faut galement modier les autres dans la mme opration atomique. Ici, avec un timing malheureux, UnsafeCachingFactorizer peut violer cet invariant. Avec des rfrences atomiques, nous ne pouvons pas modier en mme temps lastNumber et lastFactors, bien que chaque appel set() soit atomique ; il reste une fentre pendant laquelle une rfrence est modie alors que lautre ne lest pas encore et, dans
1. Tout comme AtomicLong est une classe thread-safe qui encapsule un long, AtomicReference est une classe thread-safe qui encapsule une rfrence dobjet. Les variables atomiques et leurs avantages seront prsents au Chapitre 15.

26

Les bases

Partie I

cet intervalle, dautres threads pourraient constater que linvariant nest pas vri. De mme, les deux valeurs ne peuvent pas tre lues simultanment : entre le moment o le thread A lit les deux valeurs, le thread B peut les avoir modies et, l aussi, A peut observer que linvariant nest pas vri.
Pour prserver la cohrence dun tat, vous devez modier toutes les variables de cet tat dans une unique opration atomique.

2.3.1

Verrous internes

Java fournit un mcanisme intgr pour assurer latomicit : les blocs synchronized (la visibilit est galement un autre aspect essentiel des verrous et des autres mcanismes de synchronisation ; elle sera prsente au Chapitre 3). Un bloc synchronized est form de deux parties : une rfrence un objet qui servira de verrou et le bloc de code qui sera protg par ce verrou. Une mthode synchronized est un raccourci pour un bloc synchronized qui stend tout le corps de cette mthode et dont le verrou est lobjet sur lequel la mthode est invoque (les mthodes synchronized statiques utilisent lobjet Class comme verrou).
synchronized(verrou) { // Lit ou modifie ltat partag protg par le verrou }

Tout objet Java peut implicitement servir de verrou pour les besoins dune synchronisation ; ces verrous intgrs sont appels verrous internes ou moniteurs. Le thread qui sexcute ferme automatiquement le verrou avant dentrer dans un bloc synchronized et le relche automatiquement la sortie de ce bloc, quil en soit sorti normalement ou cause dune exception. La seule faon de fermer un verrou interne consiste entrer dans un bloc ou une mthode synchronized protgs par ce verrou. Les verrous internes de Java se comportent comme des mutex (verrous dexclusion mutuelle), ce qui signie quun seul thread peut fermer le verrou. Lorsque le thread A tente de fermer un verrou qui a t verrouill par le thread B, A doit attendre, ou se bloquer, jusqu ce que B le relche. Si B ne relche jamais le verrou, A est bloqu jamais. Un bloc de code protg par un verrou donn ne pouvant tre excut que par un seul thread la fois, les blocs synchronized protgs par ce verrou sexcutent donc de faon atomique les uns par rapport aux autres. Dans le contexte de la concurrence, atomique signie la mme chose que dans les transactions un groupe dinstructions semble sexcuter comme une unit simple et indivisible. Aucun thread excutant un bloc synchronized ne peut observer un autre thread au milieu dun bloc synchronized protg par le mme verrou.

Chapitre 2

Thread safety

27

Le mcanisme de synchronisation simplie la restauration de la thread safety de la servlet de factorisation. Le Listing 2.6 synchronise la mthode service() an quun seul thread puisse entrer dans le service un instant donn. SynchronizedFactorizer est donc dsormais thread-safe. Cependant, cette approche est assez extrme puisquelle interdit lutilisation simultane de la servlet par plusieurs clients, ce qui induira des temps de rponse inacceptables. Ce problme, qui est un problme de performances, pas un problme de thread safety, sera trait dans la section 2.5.
Listing 2.6 : Servlet mettant en cache le dernier rsultat, mais avec une trs mauvaise concurrence. Ne le faites pas.
@ThreadSafe public class SynchronizedFactorizer implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; public synchronized void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber)) encodeIntoResponse (resp, lastFactors); else { BigInteger[] factors = factor(i); lastNumber = i; lastFactors = factors; encodeIntoResponse(resp, factors); } } }

2.3.2

Rentrance

Lorsquun thread demande un verrou qui est dj verrouill par un autre thread, le thread demandeur est bloqu. Les verrous internes tant rentrants, si un thread tente de prendre un verrou quil dtient dj, la requte russit. La rentrance signie que les verrous sont acquis par thread et non par appel1. Elle est implmente en associant chaque verrou un compteur dacquisition et un thread propritaire. Lorsque le compteur passe zro, le verrou est considr comme libre. Si un thread acquiert un verrou venant dtre libr, la JVM enregistre le propritaire et xe le compteur dacquisition un. Si le mme thread prend nouveau le verrou, le compteur est incrment et, lorsquil libre le verrou, le compteur est dcrment. Quand le compteur atteint zro, le verrou est relch. La rentrance facilite lencapsulation du comportement des verrous et simplie par consquent le dveloppement de code concurrent orient objet. Sans verrous rentrants, le code trs naturel du Listing 2.7, dans lequel une sous-classe rednit une mthode synchronized puis appelle la mthode de sa superclasse, provoquerait un deadlock. Les
1. Ce qui est diffrent du comportement par dfaut des mutex pthreads (POSIX threads), qui sont accords la demande.

28

Les bases

Partie I

mthodes doSomething() de Widget et LoggingWidget tant toutes les deux synchronized, chacune tente dobtenir le verrou sur le Widget avant de continuer. Si les verrous internes ntaient pas rentrants, lappel super.doSomething() ne pourrait jamais obtenir le verrou puisque ce dernier serait considr comme dj pris : le thread serait bloqu en permanence en attente dun verrou quil ne pourra jamais obtenir. Dans des situations comme celles-ci, la rentrance nous protge des interblocages.
Listing 2.7 : Ce code se bloquerait si les verrous internes ntaient pas rentrants.
public class Widget { public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething(); } }

2.4

Protection de ltat avec les verrous

Les verrous autorisant un accs srialis1 au code quils protgent, nous pouvons les utiliser pour construire des protocoles garantissant un accs exclusif ltat partag. Le respect de ces protocoles permet alors de garantir la cohrence de cet tat. Les actions composes sur ltat partag, comme lincrmentation dun compteur de visites (lire-modier-crire) ou linitialisation paresseuse (tester-puis-agir) peuvent ainsi tre rendues atomiques pour viter les situations de comptition. La dtention dun verrou pour toute la dure dune action compose rend cette action atomique. Cependant, il ne suft pas denvelopper laction compose dans un bloc synchronized : si la synchronisation sert coordonner laccs une variable, elle est ncessaire partout o cette variable est utilise. En outre, lorsque lon utilise des verrous pour coordonner laccs une variable, cest le mme verrou qui doit tre utilis chaque accs cette variable. Une erreur frquente consiste supposer que la synchronisation nest utile que lorsque lon crit dans des variables partages ; ce nest pas vrai (la section 3.1 expliquera plus clairement pourquoi).
1. Laccs srialis un objet na rien voir avec la srialisation dun objet (sa transformation en ux doctets) : un accs srialis signie que les threads doivent prendre leur tour avant davoir lexclusivit de lobjet au lieu dy accder de manire concurrente.

Chapitre 2

Thread safety

29

chaque fois quune variable dtat modiable est susceptible dtre accde par plusieurs threads, tous les accs cette variable doivent seffectuer sous la protection du mme verrou. En ce cas, on dit que la variable est protge par ce verrou.

Dans la classe SynchronizedFactorizer du Listing 2.6, lastNumber et lastFactors sont protges par le verrou interne de lobjet servlet, ce qui est document par lannotation @GuardedBy. Il nexiste aucune relation inhrente entre le verrou interne dun objet et son tat ; les champs dun objet peuvent trs bien ne pas tre protgs par son verrou interne, bien que ce soit une convention de verrouillage tout fait valide, utilise par de nombreuses classes. Possder le verrou associ un objet nempche pas les autres threads daccder cet objet la seule chose que cela empche est quun autre thread prenne le mme verrou. Le fait que tout objet dispose dun verrou interne est simplement un mcanisme pratique qui vous vite de devoir crer explicitement des objets verrous1. Cest vous de construire les protocoles de verrouillage ou les politiques de synchronisation permettant daccder en toute scurit ltat partag et cest vous de les utiliser de manire cohrente tout au long de votre programme.
Toute variable partage et modiable devrait tre protge par un et un seul verrou, et vous devez indiquer clairement aux dveloppeurs qui maintiennent votre code le verrou dont il sagit.

Une convention de verrouillage classique consiste encapsuler tout ltat modiable dans un objet et de le protger des accs concurrents en synchronisant tout le code qui accde cet tat laide du verrou interne de lobjet. Ce patron de conception est utilis dans de nombreuses classes thread-safe, comme Vector et les autres classes collections synchronises. En ce cas, toutes les variables de ltat dun objet sont protges par le verrou interne de lobjet. Cependant, ce patron nest absolument pas spcial et ni la compilation ni lexcution nimpose de patron de verrouillage2. En outre, il est relativement facile de tromper accidentellement ce protocole en ajoutant une nouvelle mthode ou un code quelconque en oubliant dutiliser la synchronisation. Toutes les donnes nont pas besoin dtre protges par des verrous ceux-ci ne sont ncessaires que pour les donnes modiables auxquelles on accdera partir de plusieurs threads. Au Chapitre 1, nous avons expliqu comment lajout dun simple vnement
1. Rtrospectivement, ce choix de conception nest probablement pas le meilleur qui soit : non seulement il peut tre trompeur, mais il force les implmentations de la JVM faire des compromis entre la taille des objets et les performances des verrous. 2. Les outils daudit du code, comme FindBugs, peuvent dtecter les cas o on accde une variable souvent, mais pas toujours, via un verrou, ce qui peut indiquer un bogue.

30

Les bases

Partie I

asynchrone comme un TimerTask pouvait imposer une thread safety qui devait se propager dans le programme, notamment si ltat de ce programme tait mal encapsul. Prenons, par exemple, un programme monothread qui traite un gros volume de donnes ; les programmes monothreads nont pas besoin de synchronisation puisquil ny a pas de donnes partages entre des threads. Imaginons maintenant que nous voulions ajouter une fonctionnalit permettant de crer priodiquement des instantans de sa progression an de ne pas devoir tout recommencer si le programme choue ou doit tre arrt. Nous pourrions pour cela choisir dutiliser un TimerTask qui se lancerait toutes les dix minutes et sauvegarderait ltat du programme dans un chier. Ce TimerTask tant appel partir dun autre thread (gr par Timer), on accde dsormais aux donnes impliques dans linstantan par deux threads : celui du programme principal et celui du Timer. Ceci signie que non seulement le code du TimerTask doit utiliser la synchronisation lorsquil accde ltat du programme, mais que tout le code du programme qui touche ces donnes doit faire de mme. Ce qui nexigeait pas de synchronisation auparavant doit maintenant lutiliser. Lorsquune variable est protge par un verrou ce qui signie que tous les accs cette variable ne pourront se faire que si lon dtient le verrou , on garantit quun seul thread pourra accder celle-ci un instant donn. Lorsquune classe a des invariants impliquant plusieurs variables dtat, il faut galement que chaque variable participant linvariant soit protge par le mme verrou car on peut ainsi modier toutes ces variables en une seule opration atomique et donc prserver linvariant. La classe Synchronized Factorizer met cette rgle en pratique : le nombre et les facteurs mis en cache sont protgs par le verrou interne de lobjet servlet.
chaque fois quun invariant implique plusieurs variables, elles doivent toutes tre protges par le mme verrou.

Si la synchronisation est un remde contre les situations de comptition, pourquoi ne pas simplement dclarer toutes les mthodes comme synchronized ? En fait, une application irrchie de synchronized pourrait fournir une synchronisation soit trop importante, soit insufsante. Se contenter de synchroniser chaque mthode, comme le fait Vector, ne suft pas rendre atomiques les actions composes dun Vector :
if (!vector.contains(element)) vector.add(element);

Cette tentative dopration mettre-si-absent contient une situation de comptition, bien que contains() et add() soient atomiques. Alors que les mthodes synchronized peuvent rendre atomiques des oprations individuelles, il faut ajouter un verrouillage lorsque plusieurs oprations sont combines pour crer une action compose (la section 4.4 prsente quelques techniques permettant dajouter en toute scurit des oprations atomiques supplmentaires des objets thread-safe). En mme temps, synchroniser

Chapitre 2

Thread safety

31

chaque mthode peut poser des problmes de vivacit ou de performances, comme nous lavons vu avec SynchronizedFactorizer.

2.5

Vivacit et performances

Dans UnsafeCachingFactorizer, nous avons ajout une mise en cache notre servlet de factorisation dans lespoir damliorer ses performances. Cette mise en cache a ncessit un tat partag qui, son tour, a exig une synchronisation pour maintenir son intgrit. Cependant, la faon dont nous avons utilis la synchronisation dans SynchronizedFactorizer fait que cette classe est peu efcace. La politique de synchronisation de SynchronizedFactorizer consiste protger chaque variable dtat laide du verrou interne de lobjet servlet ; nous lavons implmente en synchronisant lintgralit de la mthode service(). Cette approche simple et grossire a suf restaurer la thread safety, mais elle cote cher.
Figure 2.1
Concurrence inefcace pour

A B C

factor n

U L factor m U L factor m U

Synchronized Factorizer.

La mthode service() tant synchronise, un seul thread peut lexcuter la fois, ce qui va lencontre du but recherch par le framework des servlets quelles puissent traiter plusieurs requtes simultanment et peut frustrer les utilisateurs lorsque la charge est importante. En effet, si la servlet est occupe factoriser un grand nombre, les autres clients devront attendre la n de ce traitement avant quelle puisse lancer une nouvelle factorisation. Si le systme a plusieurs CPU, les processeurs peuvent rester inactifs bien que la charge soit leve. Dans tous les cas, mme des requtes courtes comme celles pour la valeur qui est dans le cache peuvent prendre un temps anormalement long si elles doivent attendre quun long calcul se soit termin. La Figure 2.1 montre ce qui se passe lorsque plusieurs requtes arrivent sur la servlet de factorisation synchronise : elles sont places en le dattente et traites squentiellement. Cette application web met en vidence une mauvaise concurrence : le nombre dappels simultans est limit non par la disponibilit des ressources de calcul mais par la structure de lapplication elle-mme. Heureusement, on peut assez simplement amliorer sa concurrence tout en la gardant thread-safe en rduisant ltendue du bloc synchronized. Vous devez faire attention ne pas trop le rduire quand mme ; une opration qui doit tre atomique doit rester dans le mme bloc synchronized. Cependant, ici il est raisonnable

32

Les bases

Partie I

dessayer dexclure du bloc les oprations longues qui naffectent pas ltat partag, an que les autres threads ne soient pas empchs daccder cet tat pendant que ces oprations sexcutent. La classe CachedFactorizer du Listing 2.8 restructure la servlet pour utiliser deux blocs synchronized distincts, chacun se limitant une petite section de code. Lun protge la squence tester-puis-agir, qui teste si lon peut se contenter de renvoyer le rsultat qui est en cache, lautre protge la mise jour du nombre et des facteurs en cache. En cadeau, nous avons rintroduit le compteur de visites et ajout un compteur de hits pour le cache, qui sont mis jour dans le bloc synchronized initial (ces compteurs constituant galement un tat modiable et partag, leurs accs doivent tre synchroniss). Les portions du code places lextrieur des blocs synchronized manipulent exclusivement des variables locales (stockes dans la pile), qui ne sont pas partages entre les threads et qui ne demandent donc pas de synchronisation.
Listing 2.8 : Servlet mettant en cache la dernire requte et son rsultat.
@ThreadSafe public class CachedFactorizer implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; @GuardedBy("this") private long hits; @GuardedBy("this") private long cacheHits; public synchronized long getHits() { return hits; } public synchronized double getCacheHitRatio() { return (double) cacheHits / (double) hits; } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = null; synchronized(this) { ++hits; if (i.equals(lastNumber)) { ++cacheHits; factors = lastFactors.clone(); } } if (factors == null) { factors = factor(i); synchronized(this) { lastNumber = i; lastFactors = factors.clone(); } } encodeIntoResponse(resp, factors); } }

CachedFactorizer nutilise plus AtomicLong pour le compteur de visite : nous sommes revenus un champ de type long. Nous aurions pu utiliser AtomicLong, mais cela avait moins dintrt que dans CountingFactorizer : les variables atomiques sont pratiques lorsque lon a besoin deffectuer des oprations atomiques sur une seule variable mais,

Chapitre 2

Thread safety

33

comme nous utilisons dj des blocs synchronized pour construire des oprations atomiques, lutilisation de deux mcanismes de synchronisation diffrents serait troublante et noffrirait aucun avantage en terme de performance et de thread safety. La restructuration de CachedFactorizer est un quilibre entre simplicit (synchronisation de toute la mthode) et concurrence (synchronisation du plus petit code possible). La fermeture et le relchement dun verrou ayant un certain cot, il est prfrable de ne pas trop dcouper les blocs synchronized (en mettant par exemple ++hits dans son propre bloc synchronized), mme si cela ne compromettrait pas latomicit. CachedFactorizer ferme le verrou lorsquil accde aux variables dtat et pendant lexcution des actions composes, mais il le libre avant dexcuter lopration de factorisation, qui peut prendre un certain temps. Cette approche permet donc de rester thread-safe sans trop affecter la concurrence ; le code contenu dans chaque bloc synchronized est "sufsamment court". Dcider de la taille des blocs synchronized implique parfois de trouver un quilibre entre la thread safety (qui ne doit pas tre compromise), la simplicit et les performances. Parfois, ces deux derniers critres sont loppos lun de lautre bien que, comme le montre CachedFactorizer, il est gnralement possible de trouver un quilibre acceptable.
Il y a souvent des tensions entre simplicit et performances. Lorsque vous implmentez une politique de synchronisation, rsistez la tentation de sacrier prmaturment la simplicit (en risquant de compromettre la thread safety) au bnce des performances.

chaque fois que vous utilisez des verrous, vous devez savoir ce que fait le code du bloc et sil est susceptible de mettre un certain temps pour sexcuter. Fermer un verrou pendant longtemps, soit parce que lon effectue un gros calcul, soit parce que lon excute une opration qui peut tre bloquante, introduit potentiellement des problmes de vivacit ou de performances.
vitez de maintenir un verrou pendant les longs traitements ou les oprations qui risquent de durer longtemps, comme les E/S sur le rseau ou la console.

3
Partage des objets
Au dbut du Chapitre 2, nous avons crit que lcriture de programmes concurrents corrects consistait essentiellement grer laccs ltat modiable et partag. Ce chapitre a expliqu comment la synchronisation permet dempcher que plusieurs threads accdent aux mmes donnes en mme temps et a examin les techniques permettant de partager et de publier des objets qui pourront tre manipuls simultanment par plusieurs threads. Lensemble de ces techniques pose les bases de la construction de classes thread-safe et dune structuration correcte des applications concurrentes laide des classes de java.util.concurrent. Nous avons galement vu comment les blocs et les mthodes synchronized permettent dassurer que les oprations sexcutent de faon atomique, mais une erreur frquente consiste penser que synchronized concerne uniquement latomicit ou la dlimitation des "sections critiques". La synchronisation a un autre aspect important et subtil : la visibilit mmoire. Nous voulons non seulement empcher un thread de modier ltat dun objet pendant quun autre thread lutilise, mais galement garantir que lorsquun thread modie ltat dun objet, les autres pourront voir les changements effectus. Or, sans synchronisation, ceci peut ne pas arriver. Vous pouvez garantir que les objets seront publis correctement soit en utilisant une synchronisation explicite, soit en tirant parti de la synchronisation intgre aux classes de la bibliothque.

3.1

Visibilit

La visibilit est un problme subtil parce que ce qui peut mal se passer nest pas vident. Dans un environnement monothread, si lon crit une valeur dans une variable et quon lise ensuite cette variable sans quelle ait t modie entre-temps, on sattend retrouver la mme valeur, ce qui semble naturel. Cela peut tre difcile accepter mais, quand les lectures et les critures ont lieu dans des threads diffrents, ce nest pas le cas. En gnral, il ny a aucune garantie que le thread qui lit verra une valeur crite

36

Les bases

Partie I

par un autre thread. Pour assurer la visibilit des critures mmoire entre les threads, il faut utiliser la synchronisation. La classe NoVisibility du Listing 3.1 illustre ce qui peut se passer lorsque des threads partagent des donnes sans synchronisation. Deux threads, le thread principal et le thread lecteur, accdent aux variables partages ready et number. Le thread principal lance le thread lecteur puis initialise number 42 et ready true. Le thread lecteur boucle jusqu voir ready true, puis afche number. Bien quil puisse sembler vident que NoVisibility afchera 42, il est possible quelle afche zro, voire quelle ne se termine jamais ! Comme cette classe nutilise pas de synchronisation adquate, il ny a aucune garantie que les valeurs de ready et number crites par le thread principal soient visibles par le thread lecteur.
Listing 3.1 : Partage de donnes sans synchronisation. Ne le faites pas.
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }

NoVisibility pourrait boucler sans n parce que le thread lecteur pourrait ne jamais voir la nouvelle valeur de ready. Ce qui est plus trange encore est que NoVisibility pourrait afcher zro car le thread lecteur pourrait voir la nouvelle valeur de ready avant lcriture dans number, en vertu dun phnomne appel rarrangement. Il ny a aucune garantie que les oprations dans un thread seront excutes dans lordre du programme du moment que ce rarrangement nest pas dtect dans ce thread mme sil est constat par les autres1. Lorsque le thread principal crit dabord dans number, puis dans ready sans synchronisation, le thread lecteur pourrait constater que les choses se sont produites dans lordre inverse ou pas du tout.

1. On pourrait penser quil sagit dune mauvaise conception, mais cela permet la JVM de proter au maximum des performances des systmes multiprocesseurs modernes. En labsence de synchronisation, par exemple, le modle mmoire de Java permet au compilateur de rordonner les oprations et de placer les valeurs en cache dans des registres ; il permet galement aux CPU de rordonner les oprations et de placer les valeurs dans des caches spciques du processeur. Le Chapitre 16 donnera plus de dtails.

Chapitre 3

Partage des objets

37

En labsence de synchronisation, le compilateur, le processeur et lenvironnement dexcution peuvent samuser bizarrement avec lordre dans lequel les oprations semblent sexcuter. Essayer de trouver lordre selon lequel les oprations sur la mmoire "doivent" se passer dans les programmes multithreads mal synchroniss sera presque certainement vou lchec.

NoVisibility est presque aussi simple quun programme concurrent peut ltre deux threads et deux variables partages , mais on peut trs bien ne pas savoir ce quil fait ni mme sil se terminera. Rchir sur des programmes concurrents mal synchroniss est vraiment trs difcile.

Tout cela peut et devrait vous effrayer. Heureusement, il existe un moyen simple dviter tous ces problmes complexes : utilisez toujours une synchronisation correcte chaque fois que des donnes sont partages par des threads. 3.1.1 Donnes obsoltes

NoVisibility a montr lune des raisons pour lesquelles les programmes insufsamment synchroniss peuvent produire des rsultats surprenants : les donnes obsoltes. Lorsque le thread lecteur examine ready, il peut voir une valeur non jour. moins dutiliser la synchronisation chaque fois que lon accde une variable, on peut lire une valeur obsolte de cette variable. Pire encore, cette obsolescence ne fonctionne pas en tout ou rien : un thread peut voir une valeur jour pour une variable et une valeur obsolte pour une autre, qui a pourtant t modie avant.

Quand une nourriture nest plus frache, elle reste gnralement comestible elle est juste moins bonne mais les donnes obsoltes peuvent tre plus dangereuses. Alors quun compteur de visites obsolte pour une application web peut ne pas tre trop mchant1, les valeurs obsoltes peuvent provoquer de svres problmes de thread safety ou de vivacit. Dans la classe NoVisibility, elles provoquaient lafchage dune valeur errone ou empchait le programme de se terminer, mais la situation peut se compliquer encore plus avec des valeurs obsoltes de rfrences dobjets, comme les liens dans une liste chane. Les donnes obsoltes peuvent provoquer de graves erreurs, difciles comprendre, comme des exceptions inattendues, des structures de donnes corrompues, des calculs imprcis et des boucles innies. La classe MutableInteger du Listing 3.2 nest pas thread-safe car on accde au champ value par get() et set() sans synchronisation. Parmi les autres risques quelle encourt,
1. Lire des donnes sans synchronisation est analogue lutilisation du niveau disolation READ _UNCOMMITTED dans une base de donnes lorsque lon veut sacrier la prcision aux performances. Cependant, dans le cas de lectures non synchronises, on sacrie un plus grand degr de prcision puisque la valeur visible dune variable partage peut tre arbitrairement obsolte.

38

Les bases

Partie I

elle peut tre victime des donnes obsoltes : si un thread appelle set(), les autres threads appelant get() pourront, ou non, constater cette modication. Nous pouvons rendre MutableInteger thread-safe en synchronisant les mthodes de lecture et dcriture comme dans la classe SynchronizedInteger du Listing 3.3. Ne synchroniser que la mthode dcriture ne serait pas sufsant : les threads appelant get() pourraient toujours voir des valeurs obsoltes.
Listing 3.2 : Conteneur non thread-safe pour un entier modifiable.
@NotThreadSafe public class MutableInteger { private int value; public int get() { return value; } public void set(int value) { this.value = value; } }

Listing 3.3 : Conteneur thread-safe pour un entier modifiable.


@ThreadSafe public class SynchronizedInteger { @GuardedBy("this") private int value; public synchronized int get() { return value; } public synchronized void set(int value) { this.value = value; } }

3.1.2

Oprations 64 bits non atomiques

Un thread lisant une variable sans synchronisation peut obtenir une valeur obsolte mais il lira au moins une valeur qui a t place l par un autre thread, plutt quune valeur alatoire. Cette garantie est appele safety magique (out-of-thin-air safety). La safety magique sapplique toutes les variables, une exception prs : les variables numriques sur 64 bits (double et long), qui ne sont pas dclares volatile (voir la section 3.1.4). En effet, le modle mmoire de Java exige que les oprations de lecture et dcriture soient atomiques, or les variables long et double sur 64 bits sont lues ou crites laide de deux oprations sur 32 bits. Si les lectures et les critures ont lieu dans des threads diffrents, il est donc possible de lire un long non volatile et dobtenir les 32 bits de poids fort dune valeur et les 32 bits de poids faible dune autre 1. Par consquent, mme si vous ne vous souciez pas des valeurs obsoltes, il nest pas prudent dutiliser des variables modiables de type long ou double dans les programmes multithreads, sauf si elle sont dclares volatile ou protges par un verrou.

1. Lorsque la spcication de la machine virtuelle Java a t crite, la plupart des processeurs du march ne disposaient pas doprations arithmtiques atomiques efcaces sur 64 bits.

Chapitre 3

Partage des objets

39

3.1.3

Verrous et visibilit

Comme le montre la Figure 3.1, le verrouillage interne peut servir garantir quun thread verra les effets dun autre thread de faon prvisible. Lorsque le thread A excute un bloc synchronized et quensuite le thread B entre dans un bloc synchronized protg par le mme verrou, les valeurs des variables visibles par A avant de librer le verrou seront visibles par B lorsquil aura pris le verrou. En dautres termes, tout ce qua fait A avant ou dans un bloc synchronized est visible par B lorsquil excute un bloc synchronized protg par le mme verrou. Sans synchronisation, cette garantie nexiste pas.
Thread A y=1

verrouille M

x=1 Tout ce qui a eu lieu avant le dverrouillage de M... dverrouille M ... est visible aprs un verrouillage de M verrouille M Thread B

i=x

dverrouille M

j=y

Figure 3.1
Visibilit garantie pour la synchronisation.

Nous pouvons maintenant donner lautre raison de la rgle qui exige que tous les threads se synchronisent sur le mme verrou lorsquils accdent une variable partage modiable garantir que les valeurs crites par un thread soient visibles par les autres. Sinon un thread lisant une variable sans dtenir le verrou appropri risque de voir une valeur obsolte.

40

Les bases

Partie I

Le verrouillage ne sert pas qu lexclusion mutuelle ; il est galement utilis pour la visibilit de la mmoire. Pour garantir que tous les threads voient les valeurs les plus rcentes des variables modiables partages, les threads de lecture et dcriture doivent se synchroniser sur le mme verrou.

3.1.4

Variables volatiles

Le langage Java fournit galement une alternative, une forme plus faible de synchronisation : les variables volatiles. Celles-ci permettent de sassurer que les modications apportes une variable seront systmatiquement rpercutes tous les autres threads. Lorsquun champ est dclar volatile, le compilateur et lenvironnement dexcution sont prvenus que cette variable est partage et que les oprations sur celle-ci ne doivent pas tre rarranges avec dautres oprations sur la mmoire. Les variables volatiles ne sont pas places dans des registres ou autres caches qui les masqueraient aux autres processeurs ; la lecture dune variable volatile renvoie donc toujours la dernire valeur qui y a t crite par un thread quelconque. Un bon moyen de se reprsenter les variables volatiles consiste imaginer quelles se comportent peu prs comme la classe SynchronizedInteger du Listing 3.3, en remplaant les oprations de lecture et dcriture sur la variable par des appels get() et set()1. Cependant, laccs une variable volatile nutilise aucun verrouillage et ne peut donc pas bloquer le thread qui sexcute : cest un mcanisme de synchronisation plus lger que synchronized2. La visibilit des variables volatiles va au-del de la valeur de la variable. Lorsquun thread A crit dans une variable volatile et quun thread B la lit ensuite, les valeurs de toutes les variables qui taient visibles pour A avant dcrire dans la variable volatile deviennent visibles pour B aprs sa lecture de cette variable. Du point de vue de la visibilit mmoire, lcriture dans une variable volatile revient donc sortir dun bloc synchronized et sa lecture revient entrer dans un bloc synchronized. Cependant, nous dconseillons de trop se er aux variables volatiles pour la visibilit dun tat quelconque ; le code qui utilise des variables volatiles dans ce but est plus fragile et plus difcile comprendre quun code qui utilise des verrous.
Nutilisez les variables volatiles que pour simplier limplmentation et la vrication de votre politique de synchronisation ; vitez-les si la vrication du code exige des calculs subtils sur la visibilit. Une bonne utilisation de ces variables consiste assurer la visibilit de leur propre tat, celui auquel se rfrent les objets, ou pour indiquer quun vnement important (comme une initialisation ou une fermeture) sest pass.

1. Cette analogie nest pas exacte car la visibilit mmoire de SynchronizedInteger est, en ralit, un peu plus forte que celle des variables volatiles, comme on lexplique au Chapitre 16. 2. Sur la plupart des processeurs actuels, les lectures volatiles sont peine un peu plus coteuses que les lectures non volatiles.

Chapitre 3

Partage des objets

41

Le Listing 3.4 illustre une utilisation typique des variables volatiles : le test dun indicateur pour savoir quand sortir dune boucle. Ici, notre thread lapparence humaine essaie de dormir aprs avoir compt des moutons. Pour que cet exemple fonctionne, lindicateur asleep doit tre dclar volatile ; sinon le thread pourrait ne pas remarquer quil a t positionn par un autre thread1. Nous aurions pu utiliser la place un verrou pour garantir la visibilit des modications apportes asleep, mais le code serait alors devenu plus lourd.
Listing 3.4 : Compter les moutons.
volatile boolean asleep; ... while (!asleep) countSomeSheep ();

Les variables volatiles sont pratiques, mais elles ont leurs limites. Le plus souvent, on les utilise pour terminer ou interrompre une excution, ou pour les indicateurs dtat comme asleep dans le Listing 3.4. Elles peuvent tre utilises dans dautres types doprations sur ltat mais, en ce cas, il faut tre plus prudent. La smantique de volatile, par exemple, nest pas sufsamment forte pour rendre une incrmentation (comme count++) atomique, sauf si vous pouvez garantir que la variable nest modie que par un seul thread (comme on lexplique au Chapitre 15, les variables atomiques permettent deffectuer des oprations lire-modier-crire et peuvent souvent tre utilises comme des "meilleures variables volatiles").
Alors que les verrous peuvent garantir la fois la visibilit et latomicit, les variables volatiles ne peuvent assurer que la visibilit.

Vous ne pouvez utiliser des variables volatiles que lorsque tous les critres suivants sont vris :
m

Les critures dans la variable ne dpendent pas de sa valeur actuelle ou vous pouvez garantir que sa valeur ne sera toujours modie que par un seul thread. La variable ne participe pas aux invariants avec dautres variables dtat.

1. Pour les applications serveur, assurez-vous de toujours utiliser loption server de la JVM lorsque vous lappelez pendant les phases de dveloppement et de tests. En mode serveur, la JVM effectue plus doptimisations quen mode client : elle extrait les invariants de boucle, par exemple. Un code qui peut sembler fonctionner dans lenvironnement de dveloppement (client JVM) peut donc ne plus fonctionner dans lenvironnement de production (serveur JVM). Dans le Listing 3.4, si nous avions par exemple "oubli" de dclarer la variable asleep comme volatile, le serveur JVM aurait pu sortir le test de la boucle (qui serait donc devenue une boucle sans n), alors que le client JVM ne laurait pas fait. Or une boucle innie apparaissant au cours du dveloppement est bien moins coteuse que si elle napparaissait qu la mise en production.

42

Les bases

Partie I

Lorsque lon accde la variable, le verrouillage nest pas ncessaire pour dautres raisons.

3.2

Publication et fuite

Publier un objet signie le rendre disponible au code qui est en dehors de sa porte courante ; stocker une rfrence vers lui o un autre code pourra le trouver, par exemple, le renvoyer partir dune mthode non prive ou le passer une mthode dune autre classe. Dans de nombreuses situations, on veut garantir que les objets et leurs dtails internes ne seront pas publis. Dans dautres, on veut publier un objet pour une utilisation gnrale, mais le faire de faon thread-safe peut ncessiter une synchronisation. Publier les variables de ltat interne dun objet peut compromettre lencapsulation et compliquer la prservation des invariants ; publier des objets avant quils ne soient totalement construits peut compromettre la thread safety. On dit quun objet qui est publi alors quil naurait pas d ltre sest chapp. La section 3.5 prsente les idiomes dune publication correcte mais, pour linstant, nous allons tudier comment un objet peut schapper. La forme la plus vidente de la publication consiste stocker une rfrence dans un champ statique public, o nimporte quelle classe et nimporte quel thread peut la voir, comme dans le Listing 3.5. Dans cet exemple, la mthode initialize() instancie un nouvel objet HashSet et le publie en stockant sa rfrence dans knownSecrets.
Listing 3.5 : Publication dun objet.
public static Set<Secret> knownSecrets; public void initialize() { knownSecrets = new HashSet<Secret>(); }

Publier un objet peut indirectement en publier dautres. Si vous ajoutez un Secret lensemble knownSecrets qui est publi, vous avez galement publi ce Secret puisque nimporte quel code peut parcourir cet ensemble et obtenir une rfrence sur le nouveau Secret. De mme, renvoyer une rfrence partir dune mthode non prive publie galement lobjet renvoy. Dans le Listing 3.6, la classe UnsafeStates publie le tableau des abrviations des tats amricains, qui est pourtant priv.
Listing 3.6 : Ltat modifiable interne la classe peut schapper. Ne le faites pas.
class UnsafeStates { private String[] states = new String[] { "AK", "AL" ... }; public String[] getStates() { return states; } }

Chapitre 3

Partage des objets

43

Publier states de cette faon pose un problme puisque nimporte quel appelant peut modier son contenu. Ici, le tableau states sest chapp de sa porte initiale car ce qui tait cens tre un tat priv a t rendu public. Publier un objet publie galement tous les objets quil rfrence dans ses champs non privs. Plus gnralement, tout objet accessible partir dun objet publi en suivant une chane de champs rfrences et dappels de mthodes non privs est galement publi. Du point de vue dune classe C, une mthode trangre est une mthode dont le comportement nest pas totalement spci par C. Cela comprend donc les mthodes des autres classes ainsi que les mthodes rednissables (donc ni private ni final) de C elle-mme. Passer un objet une mthode trangre doit galement tre considr comme une publication de cet objet. En effet, comme on ne peut pas savoir quel code sera rellement appel, on ne sait pas si la mthode trangre ne publiera pas cet objet ou si elle gardera une rfrence vers celui-ci, qui pourrait tre utilise plus tard partir dun autre thread. Quun autre thread utilise une rfrence publie nest pas vraiment le problme : ce qui importe est quil existe un risque de mauvaise utilisation1. Une fois quun objet sest chapp, vous devez supposer quune autre classe ou un autre thread peut, volontairement ou non, le dtourner. Cest un argument irrfutable en faveur de lencapsulation puisque celle-ci permet de tester si les programmes sont corrects et complique la violation accidentelle des contraintes de conception. Un dernier mcanisme par lequel un objet ou son tat interne peuvent tre publis consiste publier une instance dune classe interne, comme dans la classe ThisEscape du Listing 3.7. Quand ThisEscape publie le EventListener, elle publie implicitement aussi linstance ThisEscape car les instances des classes internes contiennent une rfrence cache vers linstance qui les englobe.
Listing 3.7 : Permet implicitement la rfrence this de schapper. Ne le faites pas.
public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener ( new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }

1. Si quelquun vole votre mot de passe et le poste sur le forum alt.free-passwords, cette information sest chappe : que quelquun lait ou non utilise pour vous causer du tort, votre compte a quand mme t compromis. Publier une rfrence expose au mme risque.

44

Les bases

Partie I

3.2.1

Pratiques de construction sres

ThisEscape illustre un cas particulier important de la fuite dun objet lorsque la rfrence this schappe lors de sa construction. Quand linstance EventListener interne est publie, linstance ThisEscape englobante lest aussi. Mais un objet nest dans un tat cohrent quaprs la n de lexcution de son constructeur : publier un objet partir de son constructeur peut donc publier un objet qui na pas t totalement construit et ceci est vrai mme si la publication seffectue dans la dernire instruction du constructeur. Si la rfrence this schappe au cours de la construction, lobjet est considr comme mal construit1.
Faites en sorte que la rfrence this ne schappe pas au cours de la construction.

Une erreur frquente pouvant conduire la fuite de this au cours de la construction consiste lancer un thread partir du constructeur. Lorsquun objet cre un thread dans son constructeur, il partage presque toujours sa rfrence this avec le nouveau thread, soit explicitement (en le passant au constructeur du thread) soit implicitement (parce que le Thread ou le Runnable est une classe interne de lobjet). Le nouveau thread pourrait alors voir lobjet englobant avant que ce dernier ne soit totalement construit. Cela ne pose aucun problme de crer un thread dans un constructeur, mais il est prfrable de ne pas lancer immdiatement ce thread : fournissez plutt une mthode start() ou initialize() qui permettra de le lancer (le Chapitre 7 dtaillera les problmes lis au cycle de vie des services). Lappel dune mthode dinstance rednissable (ni private ni final) partir du constructeur peut galement donner this loccasion de schapper. Si vous voulez enregistrer un rcepteur dvnement ou lancer un thread partir dun constructeur, vous pouvez vous en sortir en utilisant un constructeur priv et une mthode fabrique publique, comme dans la classe SafeListener du Listing 3.8.
Listing 3.8 : Utilisation dune mthode fabrique pour empcher la rfrence this de schapper au cours de la construction de lobjet.
public class SafeListener { private final EventListener listener; private SafeListener() { listener = new EventListener() { public void onEvent(Event e) { doSomething(e); } }; }

1. Plus prcisment, la rfrence this ne devrait pas schapper du thread avant la n du constructeur. Elle peut tre stocke quelque part par le constructeur tant quelle nest pas utilise par un autre thread jusqu la n de la construction. La classe SafeListener du Listing 3.8 utilise cette technique.

Chapitre 3

Partage des objets

45

public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener (safe.listener); return safe; } }

3.3

Connement des objets

Laccs des donnes partages et modiables ncessite dutiliser la synchronisation. Un bon moyen dviter cette obligation consiste ne pas partager : si on naccde aux donnes que par un seul thread, il ny a pas besoin de synchronisation. Cette technique, appele connement, est lun des moyens les plus simples qui soient pour assurer la thread safety. Lorsquun objet est conn dans un thread, son utilisation sera automatiquement thread-safe, mme si lobjet conn ne lest pas lui-mme [CPJ 2.3.2]. Swing utilise beaucoup le connement. Ses composants visuels et les objets du modle de donnes ne sont pas thread-safe, mais on obtient cette proprit en les connant au thread des vnements de Swing. Pour utiliser Swing correctement, le code qui sexcute dans les threads autres que celui des vnements ne devrait pas accder ces objets (pour faciliter cette pratique, Swing fournit le mcanisme invokeLater(), qui permet de planier lexcution dun objet Runnable dans le thread des vnements). De nombreuses erreurs de concurrence dans les applications Swing proviennent dune mauvaise utilisation de ces objets conns partir dun autre thread. Une autre application classique du connement est lutilisation des pools dobjets Connection de JDBC (Java Database Connectivity). La spcication de JDBC nexige pas que les objets Connection soient thread-safe1. Dans les applications serveur classiques, un thread prend une connexion dans le pool, lutilise pour traiter une seule requte et la retourne au pool. La plupart des requtes, comme les requtes de servlets ou les appels EJB (Enterprise JavaBeans), tant appeles de faon synchrone par un unique thread et le pool ne dlivrant pas la mme connexion un autre thread tant quelle ne sest pas termine, ce patron de gestion des connexions conne implicitement lobjet Connection ce thread pour la dure de la requte. Tout comme le langage ne possde pas de mcanisme pour imposer quune variable soit protge par un verrou, il na aucun moyen de conner un objet dans un thread. Le connement est un lment de conception du programme qui doit tre impos par son implmentation. Le langage et les bibliothques de base fournissent des mcanismes permettant de faciliter la gestion de ce connement les variables locales et la classe
1. Les implmentations du pool de connexion fournies par les serveurs dapplications sont threadsafe ; comme on accde ncessairement aux pools de connexion partir de plusieurs threads, une implmentation non thread-safe naurait aucun intrt.

46

Les bases

Partie I

ThreadLocal mais il est quand mme de la responsabilit du programmeur de sassurer que les objets conns un thread ne sen chappent pas.

3.3.1

Connement ad hoc

Le connement ad hoc intervient lorsque la responsabilit de grer le connement incombe entirement limplmentation. Il peut donc tre fragile car aucune des fonctionnalits du langage, tels les modicateurs de visibilit des membres ou les variables locales, ne facilite le connement de lobjet au thread concern. En fait, les rfrences des objets conns, comme les composants visuels ou les modles de donnes dans les applications graphiques, sont souvent contenues dans des champs publics. La dcision dutiliser le connement dcoule souvent de la dcision dimplmenter un sous-systme particulier (une interface graphique, par exemple) comme un sous-systme monothread. Ces sous-systmes ont parfois lavantage de la simplicit, ce qui compense la fragilit du connement ad hoc1. Un cas spcial de connement concerne les variables volatiles. Vous pouvez sans problme excuter des oprations lire-modier-crire sur des variables volatiles partages du moment que vous garantissez que la variable volatile nest crite qu partir dun seul thread. En ce cas, vous connez la modication dans un seul thread pour viter les situations de comptition, et les garanties de visibilit des variables volatiles assurent que les autres threads verront la dernire valeur de la variable. cause de sa fragilit, le connement ad hoc doit tre utilis avec parcimonie ; si possible, prfrez-lui plutt une des formes plus fortes du connement (connement dans la pile ou ThreadLocal). 3.3.2 Connement dans la pile

Le connement dans la pile est un cas spcial de connement dans lequel on ne peut accder un objet quau travers de variables locales. Tout comme lencapsulation aide prserver les invariants, les variables locales permettent de simplier le connement des objets un thread. En effet, les variables locales sont intrinsquement connes au thread en cours dexcution ; elles nexistent que sur la pile de ce thread, qui nest pas accessible aux autres. Le connement dans la pile (galement appel utilisation interne au thread ou locale au thread et quil ne faut pas confondre avec la classe ThreadLocal de la bibliothque standard) est plus simple maintenir et moins fragile que le connement ad hoc. Dans le cas de variables locales de types primitifs, comme numPairs dans la mthode loadTheArk() du Listing 3.9, il est impossible de violer le connement sur la pile.
1. Une autre raison de rendre un sous-systme monothread est dviter les interblocages. Cest dailleurs lune des principales raisons pour lesquelles les frameworks graphiques sont monothreads. Les sous-systmes monothreads seront prsents au Chapitre 9.

Chapitre 3

Partage des objets

47

Comme il nexiste aucun moyen dobtenir une rfrence vers une variable de type primitif, la smantique du langage garantit que les variables locales de ce type seront toujours connes dans la pile.
Listing 3.9 : Confinement des variables locales, de types primitifs ou de types rfrences.
public int loadTheArk(Collection<Animal> candidates) { SortedSet<Animal> animals; int numPairs = 0; Animal candidate = null; // animals est confin dans la mthode, ne le laissez pas schapper! animals = new TreeSet<Animal>(new SpeciesGenderComparator ()); animals.addAll(candidates); for (Animal a : animals) { if (candidate == null || !candidate.isPotentialMate (a)) candidate = a; else { ark.load(new AnimalPair(candidate, a)); ++numPairs; candidate = null; } } return numPairs; }

Maintenir un connement dans la pile pour des rfrences dobjets ncessite un petit peu plus de travail de la part du programmeur, an de sassurer que le rfrent ne schappe pas. Dans loadTheArk(), on instancie un objet TreeSet et on stocke sa rfrence dans animals. ce stade, il nexiste quune seule rfrence vers lensemble, contenue dans une variable locale et donc conne au thread en cours dexcution. Cependant, si lon publiait une rfrence cet ensemble (ou lun de ses composants internes), le connement serait viol et les animaux pourraient schapper. Lutilisation dun objet non thread-safe dans un contexte "interne un thread" est quand mme thread-safe. Cependant, vous devez tre prudent : lexigence que lobjet soit conn au thread ou le fait de savoir que lobjet conn nest pas thread-safe nexiste souvent que dans la tte du dveloppeur. Si lhypothse que lutilisation est interne au thread nest pas clairement documente, les dveloppeurs ultrieurs du code pourraient, par erreur, laisser lobjet schapper. 3.3.3

ThreadLocal

Un moyen plus formel de maintenir le connement consiste utiliser la classe Thread Local, qui permet dassocier un objet une valeur propre un thread. ThreadLocal fournit des mthodes accesseurs get() et set() qui maintiennent une copie distincte de la valeur pour chaque thread qui lutilise : un appel get() renvoie donc la valeur la plus rcente passe set() partir du thread en cours dexcution. Les variables locales au thread servent souvent empcher le partage dans les conceptions qui reposent sur des singletons modiables ou sur des variables globales. Une application

48

Les bases

Partie I

monothread, par exemple, pourrait grer une connexion globale vers une base de donnes, initialise au dmarrage, an dviter de passer un objet Connection chaque appel de mthode. Les connexions JDBC pouvant ne pas tre thread-safe, une application multithread qui utilise une connexion globale sans coordination supplmentaire nest pas thread-safe non plus. En utilisant un objet ThreadLocal pour stocker cette connexion, comme dans la classe ConnectionHolder du Listing 3.10, chaque thread disposera de sa propre connexion.
Listing 3.10 : Utilisation de ThreadLocal pour garantir le confinement au thread.
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { return DriverManager.getConnection (DB_URL); } }; public static Connection getConnection() { return connectionHolder.get(); }

Cette technique peut galement tre utilise lorsquune opration frquente a besoin dun objet temporaire comme un tampon et que lon souhaite viter de rallouer cet objet temporaire chaque appel. Avant Java 5.0, par exemple, Integer.toString() utilisait un ThreadLocal pour stocker le tampon de 12 octets utilis pour formater son rsultat au lieu dutiliser un tampon statique partag (qui aurait ncessit un verrou) ou dallouer un nouveau tampon chaque appel1. Lorsquun thread appelle ThreadLocal.get() pour la premire fois, initialValue est lue pour fournir la valeur initiale pour ce thread. Conceptuellement, vous pouvez considrer quun ThreadLocal<T> contient un Map<Thread, T> qui stocke les valeurs spciques au thread, bien quil ne soit pas implment de cette faon. Les valeurs spciques au thread sont stockes dans lobjet Thread lui-mme ; lorsquil se termine, ces valeurs peuvent tre supprimes par le ramasse-miettes. Si vous portez une application monothread vers un environnement multithread, vous pouvez prserver la thread safety en convertissant les variables globales en objets Thread Local si la smantique de ces variables le permet ; un cache au niveau de lapplication ne serait pas aussi utile sil tait transform en plusieurs caches locaux aux threads. Les implmentations des frameworks applicatifs font largement appel ThreadLocal. Les conteneurs J2EE, par exemple, associent un contexte de transaction un thread
1. Cette technique napporte probablement pas un gain en terme de performances, sauf si lopration est excute trs souvent ou que lallocation est exagrment coteuse. En Java 5.0, elle a t remplace par lapproche plus vidente qui consiste allouer un nouveau tampon chaque appel, ce qui semble indiquer que, pour quelque chose daussi banal quun tampon temporaire, cela ne permettait pas damliorer les performances.

Chapitre 3

Partage des objets

49

dexcution pour la dure dun appel EJB, ce qui peut aisment simplmenter laide dun ThreadLocal contenant le contexte de la transaction : lorsque le code du framework a besoin de savoir quelle est la transaction qui sexcute, il rcupre le contexte partir de ce ThreadLocal. Cest pratique puisque cela rduit le besoin de passer les informations sur le contexte dexcution chaque mthode, mais cela lie au framework tout code utilisant ce mcanisme. Il est assez simple dabuser de ThreadLocal en considrant sa proprit de connement comme un laisser-passer pour utiliser des variables globales ou comme un moyen de crer des paramtres de mthodes "cachs". Comme les variables globales, les variables locales aux threads peuvent contrarier la rutilisabilit du code et introduire des liens cachs entre les classes ; pour toutes ces raisons, elles doivent tre utilises avec discernement.

3.4

Objets non modiables

Lautre moyen dviter la synchronisation consiste utiliser des objets non modiables [EJ Item 13]. Quasiment tous les risques concernant latomicit et la visibilit que nous avons dcrits, comme la rcupration de valeurs obsoltes, la perte de mises jour ou lobservation dun objet dans un tat incohrent, sont lis au fait que plusieurs threads tentent daccder simultanment au mme tat modiable. Si ltat dun objet ne peut pas tre modi, tous ces risques et ces complications disparaissent. Un objet non modiable est un objet dont ltat ne peut pas tre modi aprs sa construction. Par essence, les objets non modiables sont thread-safe ; leurs invariants sont tablis par le constructeur et, si leur tat ne peut pas tre modi, ces invariants seront toujours vris.
Les objets non modiables sont toujours thread-safe.

Les objets non modiables sont simples. Ils ne peuvent tre que dans un seul tat, qui est soigneusement contrl par le constructeur. Lune des parties les plus difciles de la conception dun programme consiste prendre en compte tous les tats possibles des objets complexes ; pour ltat des objets non modiables, cette tape est triviale. Les objets non modiables sont galement plus srs. Il est dangereux de passer un objet modiable un code non vri, ou de le publier un endroit o un code suspect peut le trouver ce code pourrait modier son tat ou, pire, garder une rfrence vers lui et modier son tat plus tard, partir dun autre thread. Les objets non modiables, en revanche, ne peuvent pas tre altrs de cette manire par du code malicieux ou bogu et peuvent donc tre partags en toute scurit ou publis librement, sans quil y ait besoin de crer des copies dfensives [EJ Item 24].

50

Les bases

Partie I

Ni la spcication du langage Java ni le modle mmoire de Java ne dnissent formellement cette "immuabilit", mais elle nest pas quivalente simplement dclarer tous les champs dun objet comme final. Un objet ayant cette caractristique pourrait quand mme tre modiable puisque les champs final peuvent contenir des rfrences vers des objets modiables. 1
Un objet est non modiable si : son tat ne peut pas tre modi aprs sa construction ; tous ses champs sont final 1 ; il est correctement construit (sa rfrence this ne schappe pas au cours de la construction).

En interne, les objets non modiables peuvent quand mme utiliser des objets modiables pour grer leur tat, comme lillustre la classe ThreeStooges du Listing 3.11. Bien que le Set qui stocke les noms soit modiable, la conception de ThreeStooges rend impossible la modication de cet ensemble aprs la construction. La rfrence stooges tant final, on accde tout ltat de lobjet via un champ final. La dernire exigence, une construction correcte, est aisment vrie puisque le constructeur ne fait rien qui pourrait faire que la rfrence this devienne accessible un code autre que celui du constructeur et de celui qui lappelle.
Listing 3.11 : Classe non modiable construite partir dobjets modiables sous-jacents.
@Immutable public final class ThreeStooges { private final Set<String> stooges = new HashSet<String>(); public ThreeStooges() { stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); } public boolean isStooge(String name) { return stooges.contains(name); } }

1. Techniquement, tous les champs dun objet non modiable peuvent ne pas tre final String en est un exemple , mais cela implique de prendre en compte les situations de comptition bnignes, ce qui ncessite une trs bonne comprhension du modle mmoire de Java. Pour les curieux, String effectue un calcul paresseux du code de hachage lors du premier appel de hashCode() et place le rsultat en cache dans un champ non final ; cela ne fonctionne que parce que ce champ ne peut prendre quune seule valeur, qui sera la mme chaque calcul puisquelle est dtermine partir dun tat qui ne peut pas tre modi. Nessayez pas de faire la mme chose.

Chapitre 3

Partage des objets

51

Ltat dun programme changeant constamment, on pourrait tre tent de croire que les objets non modiables ont peu dintrt, mais ce nest pas le cas. Il y a une diffrence entre un objet non modiable et une rfrence non modiable vers celui-ci. Lorsque ltat dun programme est stock dans des objets modiables, ceux-ci peuvent quand mme tre "remplacs" par une nouvelle instance contenant le nouvel tat ; la section suivante donne un exemple de cette technique1. 3.4.1 Champs final

Le mot-cl final, une version limite du mcanisme const de C++, permet de construire des objets non modiables. Les champs final ne peuvent pas tre modis (bien que les objets auxquels ils font rfrence puissent ltre), mais ils ont galement une smantique spciale dans le modle mmoire de Java. Cest lutilisation des champs final qui rend possible la garantie dune initialisation sre (voir la section 3.5.2) qui permet daccder aux objets non modiables et de les partager sans synchronisation. Mme si un objet est modiable, rendre certains de ses champs final peut quand mme simplier la comprhension de son tat puisquen limitant la possibilit de modication dun objet on restreint galement lensemble de ses tats possibles. Un objet qui est "presque entirement modiable" mais qui possde une ou deux variables dtats modiables est plus simple quun objet qui a de nombreuses variables modiables. Dclarer des champs final permet galement dindiquer aux dveloppeurs qui maintiennent le code que ces champs ne sont pas censs tre modis.
Tout comme il est conseill de rendre tous les champs privs, sauf sils ont besoin dune visibilit suprieure [EJ Item 12], il est conseill de rendre tous les champs final, sauf sil doivent pouvoir tre modis.

3.4.2

Exemple : utilisation de volatile pour publier des objets non modiables

Dans la classe UnsafeCachingFactorizer du Listing 2.5, nous avons essay dutiliser deux AtomicReferences pour stocker le dernier nombre factoris et les derniers facteurs trouvs, mais ce ntait pas thread-safe puisque nous ne pouvions pas obtenir ou modier ces deux valeurs de faon atomique. Pour la mme raison, lutilisation de variables volatiles pour ces valeurs ne serait pas thread-safe non plus. Cependant, les objets non modiables peuvent parfois fournir une forme faible de latomicit. La servlet de
1. De nombreux dveloppeurs craignent que cette approche pose des problmes de performance, mais ces craintes sont gnralement non justies. Lallocation est moins coteuse quon ne le croit et les objets non modiables offrent des avantages supplmentaires en termes de performances, comme la rduction du besoin de verrous ou de copies dfensives, ainsi quun impact rduit sur le ramasse-miettes gnrationnel.

52

Les bases

Partie I

factorisation effectue deux oprations qui doivent tre atomiques : la mise jour du rsultat en cache et, ventuellement, la rcupration des facteurs en cache si le nombre en cache correspond au nombre soumis la requte. chaque fois quun groupe de donnes lies les unes aux autres doit tre manipul de faon atomique, pensez crer une classe non modiable pour les regrouper, comme OneValueCache1 du Listing 3.12.
Listing 3.12 : Conteneur non modifiable pour mettre en cache un nombre et ses facteurs.
@Immutable class OneValueCache { private final BigInteger lastNumber; private final BigInteger[] lastFactors; public OneValueCache(BigInteger i, BigInteger[] factors) { lastNumber = i; lastFactors = Arrays.copyOf(factors, factors.length); } public BigInteger[] getFactors(BigInteger i) { if (lastNumber == null || !lastNumber.equals(i)) return null; else return Arrays.copyOf(lastFactors, lastFactors.length); } }

Les situations de comptition lors de laccs ou de la modication de plusieurs variables lies les unes aux autres peuvent tre limines en utilisant un objet non modiable pour contenir toutes ces variables. Si cet objet tait modiable, il faudrait utiliser des verrous pour garantir latomicit ; avec un objet non modiable, un thread qui prend une rfrence sur cet objet na pas besoin de se soucier si un autre thread modiera son tat. Si les variables doivent tre modies, on cre un nouvel objet mais les ventuels threads qui manipulent lancien objet le verront quand mme dans un tat cohrent. La classe VolatileCachedFactorizer du Listing 3.13 utilise un objet OneValueCache pour stocker le nombre et les facteurs en cache. Lorsquun thread initialise le champ cache volatile pour quil contienne une rfrence un nouvel objet OneValueCache, les nouvelles valeurs en cache deviennent immdiatement visibles aux autres threads.
Listing 3.13 : Mise en cache du dernier rsultat laide dune rfrence volatile vers un objet conteneur non modifiable.
@ThreadSafe public class VolatileCachedFactorizer implements Servlet { private volatile OneValueCache cache = new OneValueCache (null, null); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = cache.getFactors(i);

1. OneValueCache ne serait pas non modiable sans les appels copyOf() dans le constructeur et la mthode daccs. Arrays.copyOf() nexiste que depuis Java 6 mais clone() devrait galement fonctionner.

Chapitre 3

Partage des objets

53

if (factors == null) { factors = factor(i); cache = new OneValueCache (i, factors); } encodeIntoResponse (resp, factors); } }

Les oprations lies au cache ne peuvent pas interfrer les unes avec les autres car OneValueCache nest pas modiable et parce quon accde au champ cache quune seule fois dans chaque partie du code. Cette combinaison dun objet conteneur non modiable pour plusieurs variables dtat lies par un invariant et dune rfrence volatile pour assurer sa visibilit rend VolatileCachedFactorizer thread-safe, bien quelle nutilise pas de verrouillage explicite.

3.5

Publication sre

Pour linstant, nous avons fait en sorte de garantir quun objet ne sera pas publi lorsquil est cens tre conn un thread ou interne un autre objet. Cependant, on veut parfois partager des objets entre les threads et, dans ce cas, il faut le faire en toute scurit. Malheureusement, se contenter de stocker une rfrence dans un champ public, comme dans le Listing 3.14, ne suft pas publier cet objet de faon satisfaisante.
Listing 3.14 : Publication dun objet sans synchronisation approprie. Ne le faites pas.
// Publication non sre public Holder holder; public void initialize() { holder = new Holder(42); }

Vous pourriez tre surpris des consquences de cet exemple qui semble pourtant anodin. cause des problmes de visibilit, lobjet Holder pourrait apparatre un autre thread sous un tat incohrent, bien que ses invariants aient t correctement tablis par son constructeur ! Cette publication incorrecte pourrait permettre un autre thread dobserver un objet partiellement construit. 3.5.1 Publication incorrecte : quand les bons objets deviennent mauvais

Vous ne pouvez pas vous er des objets qui nont t que partiellement construits. Un thread pourrait voir lobjet dans un tat incohrent et voir plus tard son tat changer brusquement, bien quil nait pas t modi depuis sa publication. En fait, si le Holder du Listing 3.15 est publi selon la publication incorrecte du Listing 3.14 et quun thread autre que celui qui publie appelle assertSanity(), celle-ci pourrait lever lexception AssertionError1 !
1. Le problme, ici, est non pas la classe Holder elle-mme, mais le fait que lobjet Holder ne soit pas correctement publi. Cependant, on pourrait immuniser Holder contre une publication incorrecte en dclarant le champ n final, ce qui rendrait Holder non modiable (voir la section 3.5.2).

54

Les bases

Partie I

Listing 3.15 : Classe risquant un problme si elle nest pas correctement publie.
public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) throw new AssertionError("This statement is false."); } }

Comme on na pas utilis de synchronisation pour rendre Holder visible aux autres threads, Holder na pas t correctement publie. Il y a deux choses qui peuvent mal se passer avec les objets mal publis. Les autres threads pourraient voir une valeur obsolte dans le champ holder et donc une rfrence null ou une autre valeur ancienne, bien quune valeur ait t place dans holder ; et, ce qui est bien pire, les autres threads pourraient voir une valeur jour pour la rfrence holder, mais des valeurs obsoltes pour ltat de lobjet Holder1. Pour rendre les choses encore moins prvisibles, un thread peut voir une valeur obsolte la premire fois quil lit un champ, puis une valeur plus jour lors de la lecture suivante, ce qui explique pourquoi assertSanity() peut lever AssertionError. Au risque de nous rpter, des phnomnes trs tranges peuvent survenir lorsque des donnes sont partages entre des threads sans synchronisation approprie. 3.5.2 Objets non modiables et initialisation sre

Les objets non modiables ayant tant dimportance, le modle mmoire de Java garantit une initialisation sre pour les partager. Comme nous lavons vu, le fait quune rfrence dobjet devienne visible pour un autre thread ne signie pas ncessairement que ltat de cet objet sera visible au thread client. Pour garantir une vue cohrente de ltat de lobjet, il faut utiliser la synchronisation. On peut en revanche accder en toute scurit un objet non modiable mme si lon nutilise pas de synchronisation pour publier sa rfrence. Pour que cette garantie dinitialisation sre puisse sappliquer, il faut que toutes les exigences portant sur les objets non modiables soient vries : tat non modiable, tous les champs dclars comme final et construction correcte (si la classe Holder du Listing avait t non modiable, assertSanity() naurait pas pu lancer AssertionError, mme si lobjet Holder tait publi incorrectement).

1. Bien quil puisse sembler que les valeurs des champs initialises dans un constructeur soient les premires crites dans ces champs et quil ny ait donc pas de valeurs "anciennes" qui puissent tre considres comme des valeurs obsoltes, le constructeur de Object initialise dabord tous les champs avec des valeurs par dfaut avant que les constructeurs des sous-classes ne sexcutent. Il est donc possible de voir la valeur par dfaut dun champ comme valeur obsolte.

Chapitre 3

Partage des objets

55

Les objets non modiables peuvent tre utiliss en toute scurit par nimporte quel thread sans synchronisation supplmentaire, mme si lon na pas utilis de synchronisation pour les publier.

Cette garantie stend aux valeurs de tous les champs final des objets correctement construits ; on peut accder ces champs en toute scurit sans synchronisation supplmentaire. Cependant, si ces champs font rfrence des objets modiables, une synchronisation sera quand mme ncessaire pour accder ltat des objets rfrencs. 3.5.3 Idiomes de publication correcte

Les objets modiables doivent tre publis correctement, ce qui implique gnralement une synchronisation entre le thread qui publie et le thread qui consomme. Pour linstant, attachons-nous vrier que le thread consommateur puisse voir lobjet dans ltat o il est publi ; nous nous occuperons plus tard de la visibilit des modications apportes aprs la publication.
Pour publier un objet correctement, il faut rendre visibles simultanment aux autres threads la fois la rfrence cet objet et son tat. Un objet correctement construit peut tre publi de faon sre en respectant lune des conditions suivantes : initialiser une rfrence dobjet laide dun initialisateur statique ; stocker une rfrence cet objet dans un champ volatile ou de type Atomic Reference ; stocker une rfrence cet objet dans un champ final dun objet correctement construit ; stocker une rfrence cet objet dans un champ protg par un verrou.

Avec la synchronisation interne des collections thread-safe, comme Vector ou synchronizedList, le placement dun objet dans ces collections vrie la dernire de ces conditions. Si le thread A place lobjet X dans une collection thread-safe et que le thread B le rcupre ensuite, il est garanti que B verra ltat de X tel que A la laiss, mme si le code qui manipule X nutilise pas de synchronisation explicite. Les collections thread-safe offrent les garanties de publication correcte suivantes, mme si la documentation Javadoc est peu claire sur ce sujet :
m

Le placement dune cl ou dune valeur dans un objet Hashtable, synchronizedMap ou ConcurrentMap est publi correctement pour tout thread qui la rcupre dans la Map (que ce soit directement ou via un itrateur). Le placement dun lment dans un objet Vector, CopyOnWriteArrayList, CopyOn WriteArraySet, synchronizedList ou synchronizedSet le publie correctement pour tout thread qui le rcupre dans la collection.

56

Les bases

Partie I

Le placement dun lment dans un objet BlockingQueue ou ConcurrentLinkedQueue le publie correctement pour tout thread qui le rcupre de la le.

Dautres mcanismes de la bibliothque des classes (comme Future et Exchanger) constituent galement une publication correcte ; nous les identierons comme tels lorsque nous les prsenterons. Lutilisation dun initialisateur statique est souvent le moyen le plus simple et le plus sr de publier des objets pouvant tre construits de faon statique :
public static Holder holder = new Holder(42);

Les initialisateurs statiques sont excuts par la JVM au moment o la classe est initialise ; cause de la synchronisation interne de la JVM, ce mcanisme garantit un publication correcte de tous les objets initialiss de cette manire [JLS 12.4.2]. 3.5.4 Objets non modiables dans les faits

Une publication correcte suft pour que dautres threads accdent en toute scurit aux objets qui ne seront pas modis sans synchronisation supplmentaire aprs leur publication. Les mcanismes de publication sre garantissent tous que ltat publi dun objet sera visible tous les threads qui y accdent ds que sa rfrence est visible ; si cet tat nest pas amen changer, cela suft assurer que tout accs cet objet est threadsafe. Les objets qui, techniquement, sont modiables mais dont ltat ne sera pas modi aprs publication sont appels objets non modiables dans les faits. Ils nont pas besoin de respecter la dnition stricte des objets non modiables de la section 3.4 ; il suft quils soient traits par le programme comme sils ntaient pas modiables aprs leur publication. Lutilisation de ce type dobjet permet de simplier le dveloppement et damliorer les performances car cela rduit les besoins de synchronisation.
Les objets non modiables dans les faits qui ont t correctement publis peuvent tre utiliss en toute scurit par nimporte quel thread sans synchronisation supplmentaire.

Les objets Date, par exemple, sont modiables1 mais, si vous les utilisez comme sils ne ltaient pas, vous pouvez vous passer du verrouillage qui serait sinon ncessaire pour partager une date entre plusieurs threads. Supposons que vous vouliez utiliser un Map pour stocker la dernire date de connexion de chaque utilisateur :
public Map<String, Date> lastLogin = Collections.synchronizedMap (new HashMap<String, Date>());

1. Ce qui est srement une erreur de conception.

Chapitre 3

Partage des objets

57

Si les valeurs Date ne sont pas modies aprs avoir t places dans le Map, la synchronisation de synchronizedMap suft pour les publier correctement et aucune synchronisation supplmentaire nest ncessaire lorsquon y accde. 3.5.5 Objets modiables

Si un objet est susceptible dtre modi aprs sa construction, une publication sre ne peut garantir la visibilit de son tat tel quil a t publi. Pour garantir la visibilit des modications ultrieures, il faut utiliser la synchronisation non seulement pour publier un objet modiable, mais galement chaque fois quon y accde. Pour partager des objets modiables en toute scurit, ceux-ci doivent avoir t publis de faon sre et tre thread-safe ou protgs par un verrou.
Les exigences de publication dun objet dpendent du fait quil soit modiable ou non : Les objets non modiables peuvent tre publis par nimporte quel mcanisme. Les objets non modiables dans les faits doivent tre publis de faon sre. Les objets modiables doivent tre publis de faon sre et tre thread-safe ou protgs par un verrou.

3.5.6

Partage dobjets de faon sre

chaque fois que lon obtient une rfrence un objet, il faut savoir ce que lon a le droit den faire. Faut-il poser un verrou avant de lutiliser ? Est-on autoris modier son tat ou peut-on seulement le lire ? De nombreuses erreurs de concurrence proviennent dune mauvaise comprhension de ces "rgles de bonne conduite" avec un objet partag. Lorsque lon publie un objet, il faut galement indiquer comment on peut y accder.
Les politiques les plus utiles pour lutilisation et le partage des objets dans un programme concurrent sont : Connement au thread. Un objet conn un thread nappartient et nest conn qu un seul thread ; il peut tre modi par le thread qui le dtient. Partage en lecture seule. Plusieurs threads peuvent accder simultanment un objet partag en lecture seule sans synchronisation supplmentaire, mais cet objet ne peut tre modi par aucun thread. Ces objets comprennent les objets non modiables et non modiables dans les faits. Partage thread-safe. Un objet thread-safe effectuant une synchronisation interne, plusieurs threads peuvent y accder via son interface publique sans synchronisation supplmentaire. Protection par verrou. On ne peut accder un objet protg par un verrou quen prenant le contrle dun verrou prcis. Ces objets comprennent ceux qui sont encapsuls dans dautres objets thread-safe et les objets publis protgs par un verrou.

4
Composition dobjets
Pour linstant, nous navons trait que des aspects de bas niveau de la scurit par rapport aux threads et de la synchronisation. Cependant, nous ne voulons pas devoir analyser chaque accs mmoire pour garantir que notre programme est thread-safe ; nous voulons pouvoir combiner des composants thread-safe pour crer des composants plus gros ou des programmes thread-safe. Ce chapitre prsente donc des patrons de structuration de classes facilitant la cration de classes thread-safe qui pourront tre maintenues sans risquer de saboter les garanties quelles offrent vis--vis des threads.

4.1

Conception dune classe thread-safe

Bien quil soit possible dcrire un programme thread-safe qui stocke tout son tat dans des champs statiques publics, vrier la thread safety dun tel programme (ou le modier pour quil reste thread-safe) est bien plus difcile quavec un programme qui utilise correctement lencapsulation. Cette dernire permet en effet de dterminer quune classe est thread-safe sans avoir besoin dexaminer tout le programme.
Le processus de conception dune classe thread-safe devrait contenir ces trois lments de base : identication des variables qui forment ltat de lobjet ; identication des invariants qui imposent des contraintes aux variables de ltat ; mise en place dune politique de gestion des accs concurrents ltat de lobjet.

Ltat dun objet commence avec ses champs. Sils sont tous de type primitif, les champs contiennent lintgralit de ltat. La classe Counter du Listing 4.1 nayant quun seul champ, son tat est donc entirement dni par la valeur de ce champ. Ltat dun objet possdant n champs de types primitifs est simplement un n-uplet des valeurs de ces

60

Les bases

Partie I

champs ; ltat dun point en deux dimensions est form des valeurs de ses coordonnes (x, y). Si certains champs dun objet sont des rfrences vers dautres objets, ltat comprend galement les champs des objets rfrencs. Ltat dun objet LinkedList, par exemple, inclut les tats de tous les objets appartenant la liste.
Listing 4.1 : Compteur mono-thread utilisant le patron moniteur de Java.
@ThreadSafe public final class Counter { @GuardedBy("this") private long value = 0; public synchronized long getValue() { return value; } public synchronized long increment() { if (value == Long.MAX_VALUE) throw new IllegalStateException ("counter overflow"); return ++value; } }

Cest la politique de synchronisation qui dnit comment un objet coordonne laccs son tat sans violer ses invariants ou ses postconditions. Elle prcise la combinaison dimmuabilit, de connement et de verrouillage quil faut utiliser pour maintenir la thread safety et indique quelles variables sont protges par quels verrous. Pour tre sr que la classe puisse tre analyse et maintenue, vous devez documenter la politique de synchronisation utilise. 4.1.1 Exigences de synchronisation

Crer une classe thread-safe implique de sassurer que ses invariants sont vris lors des accs concurrents, ce qui signie quil faut tenir compte de son tat. Les objets et les variables ont un espace dtat, lensemble des tats possibles quils peuvent prendre, et plus cet espace est rduit, plus il est facile de lapprhender. En utilisant des champs final chaque fois que cela est possible, on simplie lanalyse des tats possibles dun objet (dans le cas extrme, les objets non modiables ne peuvent tre que dans un seul tat). De nombreuses classes ont des invariants qui considrent certains tats comme valides ou invalides. Le champ value de Counter tant un long, par exemple, lespace dtat pourrait aller de Long.MIN_VALUE Long.MAX_VALUE, mais Counter ajoute une contrainte : value ne peut pas tre ngatif. De mme, les oprations peuvent avoir des postconditions qui considrent certaines transitions dtat comme incorrectes. Si ltat courant dun Counter vaut 17, par exemple, le seul tat suivant correct est 18. Lorsque ltat suivant est calcul partir de ltat courant, lopration est ncessairement compose. Toutes les oprations nimposent pas de contraintes sur les transitions dtats ; lorsque lon met jour une variable qui

Chapitre 4

Composition dobjets

61

contient la temprature courante, par exemple, son tat prcdent na pas dinuence sur le calcul. Les contraintes places sur les tats ou les transitions dtats par les invariants et les postconditions crent des besoins supplmentaires de synchronisation ou dencapsulation. Si certains tats sont invalides, alors, les variables dtat sous-jacentes doivent tre encapsules ; sinon le code client pourrait placer lobjet dans un tat invalide. Si une opration comprend des transitions dtat invalides, elle doit tre atomique. En revanche, si la classe nimpose aucune de ces contraintes, vous pouvez assouplir les besoins dencapsulation ou de srialisation, an de bncier de plus de exibilit ou de meilleures performances. Une classe peut galement possder des invariants imposant des contraintes sur plusieurs variables dtat. Une classe dintervalle numrique, comme NumberRange dans le Listing 4.10, utilise gnralement des variables dtat pour les limites infrieure et suprieure de lintervalle, et ces variables doivent obir la contrainte prcisant que la limite infrieure doit tre infrieure ou gale la limite suprieure. Des invariants multivariables comme celui-ci exigent latomicit : les variables lies entre elles doivent tre lues ou modies en une seule opration atomique. Vous ne pouvez pas en modier une, librer et reprendre le verrou, puis modier les autres car lobjet pourrait tre dans un tat invalide lorsque le verrou est libr. Lorsque plusieurs variables participent un invariant, le verrou qui les protge doit tre maintenu pendant toute la dure des oprations qui accdent ces variables.
Vous ne pouvez pas garantir la thread safety sans comprendre les invariants et les postconditions dun objet. Les contraintes sur les valeurs ou les transitions dtat autorises pour les variables dtat peuvent exiger latomicit et lencapsulation.

4.1.2

Oprations dpendantes de ltat

Les invariants de classe et les postconditions des mthodes imposent des contraintes aux tats et aux transitions dtat admis pour un objet. Certains objets possdent galement des mthodes imposant des prconditions leur tat. Vous ne pouvez pas, par exemple, supprimer un lment dune le vide ; une le doit tre dans ltat "non vide" avant de pouvoir y ter un lment. Les oprations disposant de telles prconditions sont dites dpendantes de ltat[CPJ 3]. Dans un programme monothread, lorsquune prcondition nest pas vrie, lopration na pas dautre choix que dchouer. Dans un programme concurrent, en revanche, la prcondition peut devenir vraie plus tard, par suite de laction dun autre thread. Les programmes concurrents ajoutent donc la possibilit dattendre quune prcondition soit vrie avant deffectuer lopration.

62

Les bases

Partie I

Les mcanismes intgrs permettant dattendre efcacement quune condition soit vrie wait() et notify() sont intimement lis au verrouillage interne et peuvent tre difciles utiliser correctement. Pour crer des oprations qui attendent quune prcondition devienne vraie avant de continuer, il est souvent plus simple dutiliser des classes existantes de la bibliothque standard, comme les les dattente ou les smaphores, an dobtenir le comportement souhait. Les classes bloquantes de la bibliothque, comme BlockingQueue, Semaphore et les autres synchronisateurs, sont prsentes au Chapitre 5 ; la cration de classes dpendantes de ltat utilisant les mcanismes de bas niveau fournis par la plate-forme et la bibliothque de classes est prsente au Chapitre 14. 4.1.3 Appartenance de ltat

Dans la section 4.1, nous avons laiss entendre que ltat dun objet pouvait tre un sous-ensemble des champs apparaissant dans le graphe des objets ayant cet objet pour racine. Pourquoi un sous-ensemble ? Sous quelles conditions les champs accessibles partir dun objet donn ne font pas partie de ltat de celui-ci ? Lorsque lon prcise les variables qui forment ltat dun objet, on souhaite ne prendre en compte que les donnes appartenant cet objet. Lappartenance est non pas une notion explicite du langage, mais un lment de la conception des classes. Lorsque lon alloue et remplit un HashMap, par exemple, on cre plusieurs objets : lobjet HashMap, un certain nombre dobjets Map .Entry utiliss par limplmentation de HashMap et, ventuellement, dautres objets internes. Ltat logique dun HashMap inclut ltat de tous ses Map.Entry et des objets internes, bien quils soient implments comme des objets distincts. Pour le meilleur ou pour le pire, le ramasse-miettes nous vite de devoir trop nous proccuper de lappartenance. En C++, lorsque lon passe un objet une mthode, il faut savoir si lon transfre la proprit, si lon met en place une location court terme ou si lon envisage une coproprit long terme. En Java, bien que tous ces modles dappartenance soient possibles, le ramasse-miettes rduit le cot des nombreuses erreurs lors du partage des rfrences, ce qui nous permet de rester un peu ou sur la question de lappartenance. Dans de nombreux cas, lappartenance et lencapsulation sont lies lobjet encapsule ltat quil possde et ltat quil encapsule lui appartient. Cest le propritaire dune variable dtat donne qui doit dcider du protocole de verrouillage utilis pour maintenir lintgrit de cette variable. Lappartenance implique le contrle mais, une fois que nous avons publi une rfrence vers un objet modiable, nous navons plus le contrle exclusif de cet objet ; au mieux pouvons-nous avoir une "proprit partage". Une classe ne possde gnralement pas les objets qui sont passs ses mthodes ou ses constructeurs, sauf si la mthode a t conue pour transfrer explicitement la proprit des objets qui lui sont passs (cest le cas, par exemple, des mthodes fabriques de collections synchronises).

Chapitre 4

Composition dobjets

63

Les classes Collection utilisent souvent une forme de "proprit divise" dans laquelle la collection possde ltat de linfrastructure de collection alors que le code client possde les objets stocks dans la collection. Cest le cas, par exemple, de la classe ServletContext du framework des servlets. Cette dernire fournit aux servlets un objet conteneur assimil un Map, dans lequel elles peuvent enregistrer et rcuprer des objets de niveau application par leurs noms laide des mthodes setAttribute() et getAttribute(). Lobjet ServletContext implment par le conteneur de servlets doit tre thread-safe car plusieurs threads y accderont ncessairement. Les servlets nont pas besoin dutiliser de synchronisation lorsquelles appellent setAttribute() et getAttribute(), mais elles peuvent devoir en avoir besoin lorsquelles utilisent les objets stocks dans le ServletContext. Ces objets appartiennent lapplication ; ils sont stocks par le conteneur de servlets pour le compte de lapplication et, comme tous les objets partags, ils doivent tre partags correctement ; pour empcher les interfrences dues aux accs concurrents de la part des diffrents threads, ils doivent tre soit thread-safe, soit non modiables dans les faits, soit protgs explicitement par un verrou 1.

4.2

Connement des instances

Si un objet nest pas thread-safe, plusieurs techniques permettent toutefois de lutiliser en toute scurit dans un programme multithread. Vous pouvez faire en sorte quon ny accde qu partir dun seul thread (connement au thread) ou que tous ses accs soit protgs par un verrou. Lencapsulation simplie la cration de classes thread-safe en favorisant le connement des instances, souvent simplement dsign sous le terme de connement [CPJ 2.3.3]. Lorsquun objet est encapsul dans un autre objet, tout le code qui a accs lobjet encapsul est connu et peut donc tre analys plus facilement que si lobjet tait accessible par tout le programme. En combinant ce connement une discipline de verrouillage adquate, on peut garantir que des objets qui, sans cela, ne seraient pas thread-safe seront utiliss de faon thread-safe.
Lencapsulation de donnes dans un objet conne laccs de ces donnes aux mthodes de lobjet, ce qui permet de garantir plus facilement quon accdera toujours aux donnes avec le verrou adquat.

1. Lobjet HttpSession qui effectue une fonction similaire dans le framework des servlets peut avoir des exigences plus strictes. Le conteneur de servlets pouvant accder aux objets de HttpSession an de les srialiser pour la rplication ou la passivation, ceux-ci doivent tre thread-safe puisque le conteneur y accdera en mme temps que lapplication web (nous avons crit "peut avoir" car la rplication et la passivation ne font pas partie de la spcication des servlets, bien quil sagisse dune fonctionnalit courante des conteneurs de servlets).

64

Les bases

Partie I

Les objets conns ne doivent pas schapper de leur porte. Un objet peut tre conn une instance de classe (cest le cas, par exemple, dun membre de classe priv), une porte lexicale (cest le cas des variables locales) ou un thread (cest le cas dun objet pass de mthode en mthode dans un thread, mais qui nest pas cens tre partag entre des threads). Les objets ne schappent pas tout seuls, bien sr : ils ont besoin de laide du dveloppeur, qui les aide en les publiant en dehors de leur porte. La classe PersonSet du Listing 4.2 illustre la faon dont le connement et le verrouillage peuvent fonctionner de concert pour crer une classe thread-safe, mme lorsque ses variables dtat ne le sont pas. Ltat de PersonSet est en effet gr par un HashSet qui nest pas thread-safe ; mais, comme mySet est priv et ne peut pas schapper, ce HashSet est conn dans lobjet PersonSet. Le seul code qui peut accder mySet est celui des mthodes addPerson() et containsPerson(), or chacune delles prend le verrou sur lobjet PersonSet. Comme tout son tat est protg par son verrou interne, PersonSet est thread-safe.
Listing 4.2 : Utilisation du confinement pour assurer la thread safety.
@ThreadSafe public class PersonSet { @GuardedBy("this") private final Set<Person> mySet = new HashSet<Person>(); public synchronized void addPerson(Person p) { mySet.add(p); } public synchronized boolean containsPerson(Person p) { return mySet.contains(p); } }

Cet exemple ne fait aucune supposition sur la thread safety de Person mais, si cette classe est modiable, une synchronisation supplmentaire sera ncessaire pour accder une Person rcupre partir dun PersonSet. Le moyen le plus sr serait de rendre Person thread-safe ; le moins sr serait de protger les objets Person par un verrou et de sassurer que tous les clients suivent le protocole consistant prendre le verrou adquat avant daccder une Person. Le connement dinstance est lun des moyens les plus simples pour crer des classes thread-safe. Il permet galement de choisir la stratgie de verrouillage ; PersonSet utilise son propre verrou interne pour protger son tat, mais tout verrou correctement utilis ferait de mme. Avec le connement des instances, vous pouvez aussi protger les diffrentes variables dtat par des verrous distincts (la classe ServerStatus [voir Listing 11.7] est un exemple de classe utilisant plusieurs objets verrous pour protger son tat). Les bibliothques de classes de la plate-forme contiennent plusieurs exemples de connement ; certaines classes nexistent que pour rendre thread-safe des classes qui ne le

Chapitre 4

Composition dobjets

65

sont pas. Les classes collection de base, comme ArrayList et HashMap, ne sont pas thread-safe, mais la bibliothque fournit des mthodes qui enveloppent les mthodes fabriques de ces objets (Collections.synchronizedList et dautres) pour pouvoir les utiliser en toute scurit dans des environnements multithreads. Ces fabriques utilisent le patron de conception Dcorateur (voir Gamma et al., 1995) pour envelopper la collection dans un objet synchronis ; cette enveloppe implmente chaque mthode de linterface adquate sous la forme dune mthode synchronized qui fait suivre la requte lobjet sous-jacent. Lobjet enveloppe est thread-safe tant quil dtient la seule rfrence accessible la collection sous-jacente (cette collection est donc conne dans cette enveloppe). La documentation Javadoc de ces mthodes avertit dailleurs que tous les accs la collection sous-jacente doivent se faire via lenveloppe. Il est bien sr toujours possible de violer le connement en publiant un objet cens tre conn ; sil a t conu pour tre conn dans une porte spcique, laisser lobjet schapper de cette porte constitue un bogue. Les objets conns peuvent galement schapper en publiant dautres objets comme des itrateurs ou des instances de classes internes qui peuvent, indirectement, publier les objets conns.
Le connement facilite la cration de classes thread-safe car une classe qui conne son tat peut tre analyse sans avoir besoin dexaminer tout le programme.

4.2.1

Le patron moniteur de Java

En suivant le principe du connement dinstance jusqu sa conclusion logique, on arrive au patron moniteur de Java1. Un objet respectant ce patron encapsule tout son tat modiable et le protge laide de son verrou interne. La classe Counter du Listing 4.1 est un exemple typique de ce patron. Elle encapsule une seule variable dtat, value, et tous les accs cette variable passent par ses mthodes, qui sont toutes synchronises. Le patron moniteur de Java est utilis par de nombreuses classes de la bibliothque, comme Vector et Hashtable, mais on peut parfois avoir besoin dune politique de synchronisation plus sophistique : le Chapitre 11 montrera comment amliorer la "scalabilit" des programmes laide de stratgies de verrouillage plus nes. Lavantage de ce patron est sa simplicit. Le patron moniteur de Java est simplement une convention ; nimporte quel objet verrou peut servir protger ltat dun objet pourvu quil soit utilis correctement. Le Listing 4.3 donne un exemple de classe qui utilise un verrou priv pour protger son tat.
1. Le patron moniteur de Java sinspire des travaux de Hoare sur les moniteurs (Hoare, 74), bien quil existe des diffrences signicatives entre ce patron et un vrai moniteur. Les instructions du pseudo-code pour entrer et sortir dun bloc synchronized sappellent dailleurs monitorenter et monitorexit, et les verrous internes de Java sont parfois appels verrous moniteurs ou, simplement, moniteurs.

66

Les bases

Partie I

Listing 4.3 : Protection de ltat laide dun verrou priv.


public class PrivateLock { private final Object myLock = new Object(); @GuardedBy("myLock") Widget widget; void someMethod() { synchronized(myLock) { // Accs ou modification de ltat du widget } } }

Utiliser un objet verrou priv au lieu dun verrou interne (ou tout autre verrou accessible publiquement) prsente quelques avantages. Le fait que lobjet verrou soit priv lencapsule de sorte que le code client ne peut pas le prendre, alors quun verrou public autorise le code client participer la politique de synchronisation correctement ou non. Les clients qui prennent incorrectement le verrou dun autre objet peuvent en effet poser des problmes de vivacit, et vrier quun verrou public est correctement utilis ncessite dexaminer tout le programme au lieu de cantonner lanalyse une seule classe. 4.2.2 Exemple : gestion dune otte de vhicules

La classe Counter du Listing 4.1 est un exemple dutilisation du patron moniteur de Java, mais il est un peu trop simple. Nous allons donc construire un exemple un peu moins trivial : un "gestionnaire de vhicules" charg de rpartir les vhicules dune otte comme des taxis, des voitures de police ou des camions de livraison. Nous utiliserons dabord le patron moniteur, puis nous verrons comment assouplir un peu lencapsulation tout en prservant la thread safety. Chaque vhicule est identi par une chane de caractres et a une position reprsente par les coordonnes (x, y). La classe VehicleTracker encapsule lidentit et les emplacements des vhicules connus, ce qui en fait un modle de donnes bien adapt une application graphique modle-vue-contrleur o elle peut tre partage par un thread vue et plusieurs threads modicateurs. Le thread vue rcupre les noms et les emplacements des vhicules pour les afcher :
Map<String, Point> locations = vehicles.getLocations(); for (String key : locations.keySet()) renderVehicle(key, locations.get(key));

De mme, les threads modicateurs mettent jour les emplacements des vhicules partir des donnes reues par des dispositifs GPS ou entres manuellement par un oprateur via une interface graphique :
void vehicleMoved (VehicleMovedEvent evt) { Point loc = evt.getNewLocation(); vehicles.setLocation(evt.getVehicleId(), loc.x, loc.y); }

Chapitre 4

Composition dobjets

67

Le thread vue et les threads modicateurs concourant pour accder au modle de donnes, ce dernier doit tre thread-safe. Le Listing 4.4 prsente une implmentation du gestionnaire de vhicules qui utilise le patron moniteur de Java. Pour reprsenter les emplacements des vhicules, ce gestionnaire se sert de la classe MutablePoint du Listing 4.5. Bien que MutablePoint ne soit pas thread-safe, la classe du gestionnaire lest. Ni la carte ni aucun des points modiables quelle contient ne seront jamais publis. Si lon doit renvoyer des emplacements de vhicules un client, les valeurs appropries seront copies soit laide du constructeur de copie de la classe MutablePoint, soit en utilisant la mthode deepCopy(), qui cre une nouvelle carte dont les valeurs sont des copies des cls et des valeurs de lancienne1. Cette implmentation gre en partie la thread safety en copiant les donnes modiables avant de les renvoyer au client. Cela ne pose gnralement pas de problme en terme de performances, sauf si lensemble des vhicules est trs important 2. Une autre consquence de la copie des donnes chaque appel de getLocations() est que le contenu de la collection renvoye ne changera pas, mme si les emplacements sous-jacents sont modis ; que ce soit souhaitable ou non dpend de vos besoins. Cela peut tre intressant si la cohrence interne de lensemble des emplacements est importante, auquel cas renvoyer un instantan cohrent est essentiel, mais cela peut galement tre un problme si les clients ont besoin dinformations jour pour chaque vhicule et quils doivent donc rafrachir leurs copies plus souvent.

4.3

Dlgation de la thread safety

Tous les objets, sauf les plus simples, sont des objets composs. Le patron moniteur de Java est utile lorsque lon cre des classes en partant de zro ou que lon compose des classes partir dobjets non thread-safe, mais quen est-il si les composants de notre classe sont dj thread-safe ? Faut-il ajouter une couche supplmentaire de thread safety ? La rponse est "a dpend". Dans certains cas, un objet compos partir dautres objets thread-safe est lui-mme thread-safe (voir Listings 4.7 et 4.9) ; dans dautres, cest simplement un bon point de dpart (voir Listing 4.10).
1. deepCopy() ne peut pas se contenter denvelopper la carte par un objet unmodifiableMap puisque cela ne protgerait pas la collection contre les modications ; les clients pourraient modier les objets modiables contenus dans cette carte. Pour la mme raison, le remplissage du HashMap dans deep Copy() via un constructeur de copie ne fonctionnerait pas car cela ne copierait que les rfrences aux points, pas les objets points eux-mmes. 2. deepCopy() tant appele partir dune mthode synchronise, le verrou interne du gestionnaire est dtenu pendant toute la dure de lopration de copie, qui pourrait tre assez longue. La ractivit de linterface utilisateur risque donc dtre dgrade lorsque le nombre de vhicules grs est trs grand.

68

Les bases

Partie I

Dans la classe CountingFactorizer du Listing 2.4, nous avions ajout un champ AtomicLong un objet sans tat et lobjet compos obtenu tait quand mme thread-safe. Ltat de CountingFactorizer tant ltat contenu dans le champ AtomicLong, qui est thread-safe, et CountingFactorizer nimposant aucune contrainte de validit sur le compteur, il est ais de constater que la classe est elle-mme thread-safe. Nous pourrions dire que CountingFactorizer dlgue la responsabilit de sa thread safety AtomicLong : CountingFactorizer est thread-safe parce que AtomicLong lest1.
Listing 4.4 : Implmentation du gestionnaire de vhicule reposant sur un moniteur.
@ThreadSafe public class MonitorVehicleTracker { @GuardedBy("this") private final Map<String, MutablePoint> locations; public MonitorVehicleTracker(Map<String, MutablePoint> locations) { this.locations = deepCopy(locations); } public synchronized Map<String, MutablePoint> getLocations() { return deepCopy(locations); } public synchronized MutablePoint getLocation(String id) { MutablePoint loc = locations.get(id); return loc == null ? null : new MutablePoint(loc); } public synchronized void setLocation(String id, int x, int y) { MutablePoint loc = locations.get(id); if (loc == null) throw new IllegalArgumentException ("No such ID: " + id); loc.x = x; loc.y = y; } private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) { Map<String, MutablePoint> result = new HashMap<String, MutablePoint>(); for (String id : m.keySet()) result.put(id, new MutablePoint(m.get(id))); return Collections.unmodifiableMap (result); } } public class MutablePoint { /* Listing 4.5 */ }

1. Si count navait pas t final, lanalyse de la thread safety de CountingFactorizer aurait t plus complique. Si CountingFactorizer pouvait modier count pour quil fasse rfrence un autre AtomicLong, nous devrions vrier que cette modication est visible par tous les threads qui pourraient accder au compteur et nous assurer quil ny ait pas de situation de comptition concernant la valeur de la rfrence count. Cest une autre bonne raison dutiliser des champs final chaque fois que cela est possible.

Chapitre 4

Composition dobjets

69

Listing 4.5 : Classe Point modifiable ressemblant java.awt.Point.


@NotThreadSafe public class MutablePoint { public int x, y; public MutablePoint() { x = 0; y = 0; } public MutablePoint(MutablePoint p) { this.x = p.x; this.y = p.y; } }

4.3.1

Exemple : gestionnaire de vhicules utilisant la dlgation

Construisons maintenant une version du gestionnaire de vhicules qui dlgue sa thread safety une classe thread-safe. Comme nous stockons les emplacements dans un objet Map, nous partons dune implmentation thread-safe de Map, ConcurrentHashMap. la place de MutablePoint, nous utilisons galement une classe Point non modiable (voir Listing 4.6) pour stocker chaque emplacement.
Listing 4.6 : Classe Point non modifiable utilise par DelegatingVehicleTracker.
@Immutable public class Point { public final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }

Point est thread-safe parce quelle est non modiable. Les valeurs non modiables pouvant tre librement partages et publies, nous navons plus besoin de copier les emplacements lorsquon les renvoie.

La classe DelegatingVehicleTracker du Listing 4.7 nutilise pas de synchronisation explicite ; tous les accs son tat sont grs par ConcurrentHashMap et toutes les cls et valeurs de la Map sont non modiables.
Listing 4.7 : Dlgation de la thread safety un objet ConcurrentHashMap.
@ThreadSafe public class DelegatingVehicleTracker { private final ConcurrentMap<String, Point> locations; private final Map<String, Point> unmodifiableMap ; public DelegatingVehicleTracker (Map<String, Point> points) { locations = new ConcurrentHashMap< String, Point>(points); unmodifiableMap = Collections.unmodifiableMap(locations); } public Map<String, Point> getLocations() { return unmodifiableMap ; }

70

Les bases

Partie I

Listing 4.7 : Dlgation de la thread safety un objet ConcurrentHashMap. (suite)


public Point getLocation(String id) { return locations.get(id); } public void setLocation(String id, int x, int y) { if (locations.replace(id, new Point(x, y)) == null) throw new IllegalArgumentException("invalid vehicle name: " + id); } }

Lutilisation de la classe MutablePoint au lieu de Point aurait bris lencapsulation en autorisant getLocations() publier une rfrence vers un tat modiable non thread-safe. Vous remarquerez que nous avons lgrement modi le comportement de la classe gestionnaire des vhicules : alors que la version avec moniteur renvoyait un instantan des emplacements, la version avec dlgation renvoie une vue non modiable mais "vivante" de ces emplacements. Ceci signie que, si un thread A appelle getLocations() et quun thread B modie ensuite lemplacement de lun des points, ces changements seront rpercuts dans lobjet Map renvoy au thread A. Comme nous lavons dj fait remarquer, ceci peut tre un avantage (donnes plus jour) ou un handicap (vue ventuellement incohrente de la otte) en fonction de vos besoins. Si vous avez besoin dune vue de la otte qui ne change pas, getLocations() pourrait renvoyer une copie de surface de la carte des emplacements. Le contenu de lobjet Map tant non modiable, seule sa structure a besoin dtre copie. Cest ce que lon fait dans le Listing 4.8 (o lon renvoie un vrai HashMap, puisque getLocations() na pas pour contrat de renvoyer un Map thread-safe).
Listing 4.8 : Renvoi dune copie statique de lensemble des emplacements au lieu dune copie "vivante".
public Map<String, Point> getLocations() { return Collections.unmodifiableMap ( new HashMap<String, Point>(locations)); }

4.3.2

Variables dtat indpendantes

Les exemples de dlgation que nous avons donns jusqu prsent ne dlguaient qu une seule variable dtat thread-safe, mais il est galement possible de dlguer la thread safety plusieurs variables dtat sous-jacentes du moment que ces dernires sont indpendantes, ce qui signie que la classe compose nimpose aucun invariant impliquant les diffrentes variables dtat. La classe VisualComponent du Listing 4.9 est un composant graphique qui permet aux clients denregistrer des couteurs (listeners) pour les vnements souris et clavier. Elle gre donc deux listes dcouteurs, une pour chaque type dvnement, an que les couteurs appropris puissent tre appels lorsquun vnement survient. Cependant, il ny a aucune relation entre lensemble des couteurs de la souris et celui des couteurs

Chapitre 4

Composition dobjets

71

du clavier ; ils sont tous les deux indpendants et VisualComponent peut donc dlguer ses obligations de thread safety aux deux listes thread-safe sous-jacentes.
Listing 4.9 : Dlgation de la thread plusieurs variables dtat sous-jacentes.
public class VisualComponent { private final List<KeyListener> keyListeners = new CopyOnWriteArrayList <KeyListener>(); private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList <MouseListener>(); public void addKeyListener(KeyListener listener) { keyListeners.add(listener); } public void addMouseListener (MouseListener listener) { mouseListeners.add(listener); } public void removeKeyListener (KeyListener listener) { keyListeners.remove(listener); } public void removeMouseListener (MouseListener listener) { mouseListeners.remove(listener); } }

VisualComponent utilise un objet CopyOnWriteArrayList pour stocker chaque liste dcouteurs ; cette classe est une implmentation thread-safe de List particulirement bien adapte ce type de gestion (voir la section 5.2.3). Chaque liste est thread-safe et, comme il ny a pas de contrainte reliant ltat de lune ltat de lautre, Visual Component peut dlguer ses responsabilits de thread safety aux objets mouseListeners et keyListeners.

4.3.3

checs de la dlgation

La plupart des classes composes ne sont pas aussi simples que VisualComponent : leurs invariants lient entre elles les variables dtat de leurs composants. Pour grer son tat, la classe NumberRange du Listing 4.10 utilise deux AtomicInteger mais impose une contrainte supplmentaire : le premier nombre doit tre infrieur ou gal au second.
Listing 4.10 : Classe pour des intervalles numriques, qui ne protge pas suffisamment ses invariants. Ne le faites pas.
public class NumberRange { // INVARIANT: lower <= upper private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) { // Attention : tester-puis-agir non sr if (i > upper.get()) throw new IllegalArgumentException("cant set lower to " + i + " > upper");

72

Les bases

Partie I

Listing 4.10 : Classe pour des intervalles numriques, qui ne protge pas suffisamment ses invariants. Ne le faites pas. (suite)
lower.set(i); } public void setUpper(int i) { // Attention : tester-puis-agir non sr if (i < lower.get()) throw new IllegalArgumentException("cant set upper to " + i + " < lower"); upper.set(i); } public boolean isInRange(int i) { return (i >= lower.get() && i <= upper.get()); } }

NumberRange nest pas thread-safe car elle ne prserve pas linvariant qui contraint les valeurs de lower et upper. Les mthodes setLower() et setUpper() tentent bien de le respecter, mais elles le font mal car ce sont toutes les deux des squences tester-puis-agir qui nutilisent pas un verrouillage sufsant pour tre atomiques. Si le nombre contient (0, 10) et quun thread appelle setLower(5) pendant quun autre appelle setUpper(4), un timing malheureux fera que les deux tests russiront et que ces deux modications seront donc autorises. Le rsultat sera alors un intervalle (5, 4), donc dans un tat incorrect. Bien que les AtomicInteger sous-jacents soient thread-safe, la classe compose ne lest donc pas. Les variables dtat lower et upper ntant pas indpendantes, NumberRange ne peut pas se contenter de dlguer sa thread safety ses variables dtat thread-safe. NumberRange pourrait tre rendue thread-safe en utilisant le mme verrou pour protger lower et upper et donc ses invariants. Elle doit galement viter de publier lower et upper pour empcher le code client de pervertir les invariants.

Si une classe comprend des actions composes, comme cest le cas de NumberRange, la dlgation seule ne suft pas pour assurer la thread safety. La classe doit fournir son propre verrouillage pour garantir que ces oprations sont atomiques, sauf si laction compose peut entirement tre dlgue aux variables dtat sous-jacentes.
Si une classe contient plusieurs variables dtat thread-safe indpendantes et quelle nait aucune opration ayant des transitions dtat invalides, elle peut dlguer sa thread safety ces variables dtat.

Le problme qui empchait NumberRange dtre thread-safe bien que ses composants dtat fussent thread-safe ressemble beaucoup lune des rgles sur les variables volatiles de la section 3.1.4 : une variable ne peut tre dclare volatile que si elle ne participe pas des invariants impliquant dautres variables dtat.

Chapitre 4

Composition dobjets

73

4.3.4

Publication des variables dtat sous-jacentes

Lorsque lon dlgue la thread safety aux variables sous-jacentes dun objet, sous quelles conditions peut-on publier ces variables pour que les autres classes puissent galement les modier ? L encore, la rponse dpend des invariants quimpose la classe sur ces variables. Bien que le champ value sous-jacent de Counter puisse recevoir nimporte quelle valeur entire, Counter la contraint ne prendre que des valeurs positives et lopration dincrmentation contraint lensemble des tats suivants admis pour un tat donn. Si le champ value tait public, les clients pourraient le modier et y placer une valeur invalide ; si ce champ tait publi, il rendrait la classe incorrecte. En revanche, si une variable reprsente la temprature courante ou lidentiant du dernier utilisateur stre connect, le fait quune autre classe modie sa valeur ne violera probablement pas les invariants et la publication de cette variable peut donc tre acceptable (elle peut quand mme tre dconseille, car la publication de variables modiables contraint les futurs dveloppements et les occasions de crer des sous-classes, mais cela ne rendrait pas ncessairement la classe non thread-safe).
Une variable dtat thread-safe ne participant aucun invariant qui contraint sa valeur et dont aucune des oprations ne possde de transition dtat interdit peut tre publie sans problme.

On pourrait publier sans problme, par exemple, les champs mouseListeners ou keyListeners de VisualComponent. Cette classe nimposant aucune contrainte sur les tats admis pour ses listes dcouteurs, ces champs pourraient tre publics ou publis sans compromettre la thread safety. 4.3.5 Exemple : gestionnaire de vhicules publiant son tat

Nous allons construire une version du gestionnaire de vhicules qui publie son tat modiable. Nous devons donc modier nouveau linterface pour prendre en compte ce changement, cette fois-ci en utilisant des points modiables mais thread-safe.
Listing 4.11 : Classe point modifiable et thread-safe.
@ThreadSafe public class SafePoint { @GuardedBy("this") private int x, y; private SafePoint(int[] a) { this(a[0], a[1]); } public SafePoint(SafePoint p) { this(p.get()); } public SafePoint(int x, int y) { this.set(x, y) }

74

Les bases

Partie I

Listing 4.11 : Classe point modifiable et thread-safe. (suite)


public synchronized int[] get() { return new int[] { x, y }; } public synchronized void set(int x, int y) { this.x = x; this.y = y; } }

La classe SafePoint du Listing 4.11 fournit un accesseur de lecture qui rcupre les deux valeurs x et y et les renvoie dans un tableau de deux lments1. Si nous avions fourni des accesseurs de lecture distincts pour x et y, les valeurs pourraient tre modies entre le moment o lon rcupre une coordonne et celui o lon rcupre lautre : le code client verrait alors une valeur incohrente, cest--dire un emplacement ( x, y) o le vhicule naurait jamais t. Grce SafePoint, nous pouvons construire un gestionnaire de vhicules qui publie son tat modiable sans compromettre la scurit par rapport aux threads, comme le montre la classe PublishingVehicleTracker du Listing 4.12.
Listing 4.12 : Gestionnaire de vhicule qui publie en toute scurit son tat interne.
@ThreadSafe public class PublishingVehicleTracker { private final Map<String, SafePoint> locations; private final Map<String, SafePoint> unmodifiableMap ; public PublishingVehicleTracker(Map<String, SafePoint> locations) { this.locations = new ConcurrentHashMap <String, SafePoint>(locations); this.unmodifiableMap = Collections.unmodifiableMap(this.locations); } public Map<String, SafePoint> getLocations() { return unmodifiableMap ; } public SafePoint getLocation(String id) { return locations.get(id); } public void setLocation(String id, int x, int y) { if (!locations.containsKey(id)) throw new IllegalArgumentException ("invalid vehicle name: " + id); locations.get(id).set(x, y); } }

PublishingVehicleTracker tire sa thread safety de la dlgation un objet Concurrent HashMap sous-jacent mais, cette fois, le contenu de la Map sont des points modiables thread-safe et non plus des points non modiables. La mthode getLocation() renvoie
1. Le constructeur est priv pour viter la situation de comptition qui surviendrait si le constructeur de copie tait implment sous la forme this(p.x, p.y) ; cest un exemple de lidiome capture du constructeur priv (Bloch et Gafter, 2005).

Chapitre 4

Composition dobjets

75

une copie non modiable de la Map sous-jacente. Le code appelant ne peut ni ajouter ni supprimer de vhicules, mais il pourrait modier lemplacement dun vhicule en modiant les valeurs SafePoint contenues dans la Map. Ici aussi, la nature "vivante" de la Map peut tre un avantage ou un inconvnient en fonction des besoins. Publishing VehicleTracker est thread-safe mais elle ne le serait pas si elle imposait des contraintes supplmentaires sur les valeurs valides des emplacements des vhicules. Sil faut pouvoir placer un "veto" sur les modications apportes aux emplacements des vhicules ou effectuer certaines actions lorsquun emplacement est modi, lapproche choisie par PublishingVehicleTracker ne convient pas.

4.4

Ajout de fonctionnalits des classes thread-safe existantes

La bibliothque de classes de Java contient de nombreuses classes "briques" utiles. Il est souvent prfrable de rutiliser des classes existantes plutt quen crer de nouvelles : la rutilisation permet de rduire le travail et les risques de dveloppement (puisque les composants existants ont dj t tests), ainsi que les cots de maintenance. Parfois, une classe thread-safe disposant de toutes les oprations dont on a besoin existe dj mais, souvent, le mieux que lon puisse trouver est une classe fournissant presque toutes les oprations voulues : nous devons alors ajouter une nouvelle opration sans compromettre son comportement vis--vis des threads. Supposons, par exemple, que nous ayons besoin dune liste thread-safe disposant dune opration ajouter-si-absent atomique. Les implmentations synchronises de List font laffaire puisquelles fournissent les mthodes contains() et add() partir desquelles nous pouvons construire lopration voulue. Le concept ajouter-si-absent est assez vident : on teste si un lment se trouve dans la collection avant de lajouter et on ne lajoute pas sil est dj prsent (vos voyants dalarme de tester-puis-agir devraient dj stre allums). Le fait que la classe doive tre thread-safe ajoute une autre contrainte : les oprations comme ajouter-si-absent doivent tre atomiques. Toute interprtation sense suggre que, si vous prenez une List qui ne contient pas lobjet X et que vous lui ajoutiez deux fois X avec ajouter-si-absent, la collection ne contiendra nalement quune seule copie de X. Si ajouter-si-absent ntait pas atomique et avec un timing malheureux, deux threads pourraient constater que X ntait pas prsent et lajouteraient tous les deux. Le moyen le plus simple dajouter une nouvelle opration atomique consiste modier la classe initiale pour quelle dispose de cette opration, mais ce nest pas toujours possible car vous pouvez ne pas avoir accs au code source ou ne pas avoir le droit de le modier. Si vous pouvez modier la classe originale, vous devez comprendre la politique de synchronisation de son implmentation pour rester cohrent avec sa conception initiale. Pour ajouter directement une nouvelle mthode la classe, il faut aussi que tout

76

Les bases

Partie I

le code qui implmente la politique de synchronisation de cette classe soit contenu dans le mme chier source, an de faciliter sa comprhension et sa maintenance. Une autre approche consiste tendre la classe en supposant quelle a t conue pour pouvoir tre tendue. La classe BetterVector du Listing 4.13, par exemple, tend Vector pour lui ajouter une mthode putIfAbsent(). tendre Vector est assez simple, mais toutes les classes nexposent pas assez leur tat aux sous-classes pour quil en soit toujours ainsi. Lextension dune classe est plus fragile que lajout direct de code cette classe car elle implique la distribution de la politique de synchronisation entre plusieurs chiers sources distincts. Si la classe sous-jacente modie ensuite sa politique de synchronisation en choisissant un verrou diffrent pour protger ses variables dtat, la sous-classe tomberait en panne sans prvenir puisquelle nutiliserait plus le bon verrou pour contrler les accs concurrents ltat de sa classe de base (la politique de synchronisation de Vector tant xe par sa spcication, BetterVector ne peut pas souffrir de ce problme).
Listing 4.13 : Extension de Vector pour disposer dune mthode ajouter-si-absent.
@ThreadSafe public class BetterVector<E> extends Vector<E> { public synchronized boolean putIfAbsent(E x) { boolean absent = !contains(x); if (absent) add(x); return absent; } }

4.4.1

Verrouillage ct client

Pour un objet ArrayList envelopp dans un objet Collections.synchronizedList, aucune des deux approches prcdentes ne peut fonctionner puisque le code client ne connat mme pas la classe de lobjet List renvoy par les fabriques de lenveloppe synchronise. Une troisime stratgie consiste tendre la fonctionnalit de la classe sans ltendre elle-mme mais en plaant le code dextension dans une classe auxiliaire. Le Listing 4.14 prsente une tentative manque de crer une classe auxiliaire dote dune opration ajouter-si-absent atomique portant sur une List thread-safe.
Listing 4.14 : Tentative non thread-safe dimplmenter ajouter-si-absent. Ne le faites pas.
@NotThreadSafe public class ListHelper<E> { public List<E> list = Collections.synchronizedList(new ArrayList<E>()); ... public synchronized boolean putIfAbsent(E x) { boolean absent = !list.contains(x); if (absent) list.add(x); return absent; } }

Chapitre 4

Composition dobjets

77

Pourquoi cela ne fonctionne-t-il pas, bien que putIfAbsent() soit synchronise ? Le problme est que cette mthode se synchronise sur le mauvais verrou. Quel que soit celui que List utilise pour protger son tat, il est certain que ce nest pas le mme que celui de ListHelper. Cette dernire ne fournit donc quune illusion de synchronisation : les diffrentes oprations sur les listes, bien quelles soient toutes synchronises, utilisent des verrous diffrents, ce qui signie que putIfAbsent() nest pas atomique par rapport aux autres oprations sur la List. Il ny a donc aucune garantie quun autre thread ne pourra modier la liste pendant lexcution de putIfAbsent(). Pour que cette approche fonctionne, vous devez utiliser le mme verrou que List en vous servant dun verrouillage ct client ou externe. Grce au verrouillage ct client, un code client qui utilise un objet X sera protg avec le verrou que X utilise pour protger son propre tat. Pour mettre en place ce type de verrouillage, il faut connatre le verrou que X utilise. La documentation de Vector et des classes enveloppes synchronises indique indirectement que ces classes supportent le verrouillage ct client en utilisant le verrou interne du Vector ou de la collection enveloppe (pas celui de la collection enveloppe). Le Listing 4.15 prsente une mthode putIfAbsent() qui utilise correctement le verrouillage ct client lorsquelle sapplique une List thread-safe.
Listing 4.15 : Implmentation dajouter-si-absent avec un verrouillage ct client.
@ThreadSafe public class ListHelper<E> { public List<E> list = Collections.synchronizedList(new ArrayList<E>()); ... public boolean putIfAbsent(E x) { synchronized (list) { boolean absent = !list.contains(x); if (absent) list.add(x); return absent; } } }

Si lextension dune classe en lui ajoutant une autre opration atomique est fragile parce quelle distribue le code de verrouillage entre plusieurs classes dune hirarchie dobjets, le verrouillage ct client lest plus encore puisquil implique de placer le code de verrouillage dune classe C dans des classes qui nont rien voir avec C. Faites attention lorsque vous utilisez un verrouillage ct client sur des classes qui ne prcisent pas leur stratgie de verrouillage. Le verrouillage ct client a beaucoup de points communs avec lextension de classe : tous les deux lient le comportement de la classe drive limplmentation de la classe de base. Tout comme lextension viole lencapsulation de limplmentation [EJ Item 14], le verrouillage ct client viole lencapsulation de la politique de synchronisation.

78

Les bases

Partie I

4.4.2

Composition

Il existe une autre solution, moins fragile, pour ajouter une opration atomique une classe existante : la composition. La classe ImprovedList du Listing 4.16 implmente les oprations de List en les dlguant une instance sous-jacente de List et ajoute une opration putIfAbsent() (comme Collections.synchronizedList et les autres collections enveloppes, ImprovedList suppose que le client nutilisera pas directement la liste sous-jacente aprs lavoir passe au constructeur de lenveloppe et quil ny accdera que par lobjet ImprovedList).
Listing 4.16 : Implmentation dajouter-si-absent en utilisant la composition.
@ThreadSafe public class ImprovedList<T> implements List<T> { private final List<T> list; public ImprovedList(List<T> list) { this.list = list; } public synchronized boolean putIfAbsent(T x) { boolean contains = list.contains(x); if (!contains) list.add(x); return !contains; } public synchronized void clear() { list.clear(); } // ... dlgations similaires pour les autres mthodes de List }

ImprovedList ajoute un niveau de verrouillage supplmentaire en utilisant son propre verrou interne. Peu importe que la List sous-jacente soit thread-safe puisque Improved List fournit son propre verrouillage an dassurer la thread safety, mme si la List nest pas thread-safe ou quelle modie limplmentation de son verrouillage. Bien que cette couche de synchronisation additionnelle puisse avoir un lger impact sur les performances1, limplmentation de ImprovedList est moins fragile que les tentatives de mimer la stratgie de verrouillage dun autre objet. En ralit, nous avons utilis le patron moniteur de Java pour encapsuler une List existante et cela garantit la thread safety tant que notre classe contient la seule rfrence existante cette List sousjacente.

4.5

Documentation des politiques de synchronisation

La documentation est lun des outils les plus puissants qui soit (et malheureusement lun des moins utiliss) pour grer la thread safety. Cest dans la documentation que les utilisateurs recherchent si une classe est thread-safe et que les dveloppeurs qui maintiennent le code essaient de comprendre la stratgie de limplmentation pour viter de la
1. Cet impact sera peu important car il ny aura pas de concurrence sur la synchronisation de la List sous-jacente. Celle-ci sera donc rapide ; voir le Chapitre 11.

Chapitre 4

Composition dobjets

79

compromettre accidentellement. Malheureusement, la documentation ne contient souvent pas les informations dont on a besoin.
Documentez les garanties de thread safety dune classe pour ses clients et documentez sa politique de synchronisation pour ses dveloppeurs ultrieurs.

Chaque utilisation de synchronized, volatile ou dune classe thread-safe est le reet dune politique de synchronisation qui dnit une stratgie assurant lintgrit des donnes vis--vis des accs concurrents. Cette politique est un lment de la conception dun programme et devrait apparatre dans la documentation. Le meilleur moment pour documenter des dcisions de conception est, videmment, la phase de conception. Quelques semaines ou mois plus tard, les dtails peuvent stre estomps et cest la raison pour laquelle il faut les crire avant de les oublier. Mettre au point une politique de synchronisation exige de prendre un certain nombre de dcisions : quelles variables rendre volatiles, quelles variables protger par des verrous, quel(s) verrou(s) protge(nt) quelles variables, quelles variables faut-il rendre non modiables ou conner dans un thread, quelles oprations doivent tre atomiques, etc. ? Certaines de ces dcisions sont strictement des dtails dimplmentation et devraient tre documentes pour aider les futurs dveloppeurs, mais dautres affectent le comportement du verrouillage observable dune classe et devraient tre documentes comme une partie de sa spcication. Au minimum, documentez les garanties de thread safety offertes par la classe. Est-elle thread-safe ? Appelle-t-elle des fonctions de rappel pendant quelle dtient un verrou ? Y a-t-il des verrous prcis qui affectent son comportement ? Nobligez pas les clients faire des suppositions risques. Si vous ne voulez pas fournir dinformation pour le verrouillage ct client, parfait, mais indiquez-le. Si vous souhaitez que les clients puisse crer de nouvelles oprations atomiques sur votre classe, comme nous lavons fait dans la section 4.4, indiquez les verrous quils doivent obtenir pour le faire en toute scurit. Si vous utilisez des verrous pour protger ltat, signalez-le dans la documentation pour prvenir les futurs dveloppeurs, dautant que cest trs facile : lannotation @GuardedBy sen chargera. Si vous utilisez des moyens plus subtils pour maintenir la thread safety, indiquez-les car cela peut ne pas apparatre vident aux dveloppeurs chargs de maintenir le code. La situation actuelle de la documentation de la scurit vis--vis des threads, mme pour les classes de la bibliothque standard, nest pas encourageante. Combien de fois avezvous recherch une classe dans Javadoc en vous demandant nalement si elle tait threadsafe1 ? La plupart des classes ne donnent aucun indice. De nombreuses spcications
1. Si vous ne vous ltes jamais demand, nous admirons votre optimisme.

80

Les bases

Partie I

ofcielles des technologies Java, comme les servlets et JDBC, sous-documentent cruellement leurs promesses et leurs besoins de thread safety. Bien que la prudence suggre de ne pas supposer des comportements qui ne fassent pas partie de la spcication, il faut bien que le travail soit fait et nous devons souvent choisir entre plusieurs mauvaises suppositions. Doit-on supposer quun objet est thread-safe parce quil nous semble quil devrait ltre ? Doit-on supposer que laccs un objet peut tre rendu thread-safe en obtenant dabord son verrou (cette technique risque ne fonctionne que si nous contrlons tout le code qui accde cet objet ; sinon elle ne fournit quune illusion de thread safety) ? Aucun de ces choix nest vraiment satisfaisant. Pour accentuer le tout, notre intuition peut parfois nous tromper sur les classes qui sont "probablement thread-safe" et celles qui ne le sont pas. java.text.SimpleDateFormat, par exemple, nest pas thread-safe, mais sa documentation Javadoc oubliait de le mentionner jusquau JDK 1.4. Que cette classe prcise ne pas tre thread-safe a pris par surprise de nombreux dveloppeurs. Combien de programmes ont cr par erreur une instance partage dun objet non thread-safe et lont utilise partir de plusieurs threads, sans savoir que cela pourrait donner des rsultats incorrects en cas de lourde charge ? Un problme comme celui de SimpleDateFormat pourrait tre vit en ne supposant pas quune classe soit thread-safe si elle nindique pas quelle lest. En revanche, il est impossible de dvelopper une application utilisant des servlets sans faire quelques suppositions assez raisonnables sur la thread safety des objets conteneurs comme HttpSession. Nobligez pas vos clients ou vos collgues faire ce genre de supposition. 4.5.1 Interprtation des documentations vagues

Les spcications de nombreuses technologies Java ne disent rien, ou trs peu de choses, sur les garanties et les besoins de thread safety dinterfaces comme ServletContext, HttpSession ou DataSource1. Ces interfaces tant implmentes par lditeur du conteneur ou de la base de donnes, il est souvent impossible de consulter le code source pour savoir ce quil fait. En outre, on ne souhaite pas se er aux dtails dimplmentation dun pilote JDBC particulier : on veut respecter le standard pour que notre code fonctionne correctement avec nimporte quel pilote. Cependant, les mots "thread" et "concurrent" napparaissent jamais dans la spcication de JDBC et trs peu dans celle des servlets. Comment faire dans une telle situation ? Il faut faire des suppositions. Un moyen damliorer la qualit de ces dductions consiste interprter la spcication du point de vue de celui charg de limplmenter (un diteur de conteneur ou de base de donnes, par exemple) et non du point de vue de celui qui se contentera de lutiliser. Les servlets tant toujours appeles partir dun thread gr par un conteneur, il est raisonnable de penser que ce conteneur saura sil y a
1. Il est dailleurs particulirement frustrant que ces omissions perdurent malgr les nombreuses rvisions majeures de ces spcications.

Chapitre 4

Composition dobjets

81

plusieurs threads. Le conteneur de servlet mettant disposition des objets qui offrent des services plusieurs servlets, comme HttpSession ou ServletContext, il devrait sattendre ce quon accde ces objets de faon concurrente puisquil a cr plusieurs threads qui appelleront des mthodes comme Servlet.service() et que celles-ci accdent srement lobjet ServletContext. Comme il est impossible dimaginer que ces objets puissent tre utiliss dans un contexte monothread, on doit supposer quils ont t conus pour tre thread-safe, bien que la spcication ne lexige pas explicitement. En outre, sils demandaient un verrouillage ct client, quel est le verrou que le client devrait utiliser pour synchroniser son code ? La documentation ne le dit pas et on imagine mal comment le deviner. Cette "supposition raisonnable" est conrme par les exemples de la spcication et des didacticiels ofciels, qui montrent comment accder un objet ServletContext ou HttpSession sans utiliser la moindre synchronisation ct client. En revanche, les objets placs avec setAttribute() dans un objet ServletContext ou HttpSession appartiennent lapplication web, pas au conteneur de servlets. La spcication des servlets ne suggre aucun mcanisme pour coordonner les accs concurrents aux attributs partags. Les attributs stocks par le conteneur pour le compte de lapplication devraient donc tre thread-safe ou non modiables dans les faits. Si le conteneur se contentait de les stocker pour lapplication web, une autre possibilit serait de sassurer quils sont correctement protgs par un verrou lorsquils sont utiliss par le code de lapplication servlet. Cependant, le conteneur pouvant vouloir srialiser les objets dans HttpSession pour des besoins de rplication ou de passivation et le conteneur de servlets ne pouvant pas connatre votre protocole de verrouillage, cest vous de les rendre thread-safe. On peut faire le mme raisonnement pour linterface DataSource de JDBC, qui reprsente un pool de connexions rutilisables. Un objet DataSource fournissant un service une application, il serait surprenant que cela se passe dans un contexte monothread : il est en effet difcilement imaginable que cela nimplique pas un appel getConnection() partir de plusieurs threads. Dailleurs, comme pour les servlets, la spcication JDBC ne suggre pas de verrouillage ct client dans les nombreux exemples de code qui utilisent DataSource. Par consquent, bien que la spcication ne promette pas que DataSource soit thread-safe et nexige pas que les diteurs de conteneurs fournissent une implmentation thread-safe, nous sommes obligs de supposer que DataSource .getConnection() nexige pas de verrouillage ct client. En revanche, on ne peut pas en dire autant des objets Connection JDBC fournis par DataSource puisquils ne sont pas ncessairement prvus pour tre partags par dautres activits tant quils ne sont pas revenus dans le pool. Si une activit obtient un objet Connection JDBC et quelle se dcompose en plusieurs threads, elle doit prendre la

82

Les bases

Partie I

responsabilit de garantir que laccs cet objet est correctement protg par synchronisation (dans la plupart des applications, les activits qui utilisent un objet Connection JDBC sont implmentes de sorte conner cet objet dans un thread particulier).

5
Briques de base
Le chapitre prcdent a explor plusieurs techniques pour construire des classes threadsafe, notamment la dlgation de la thread safety des classes thread-safe existantes. Lorsquelle est applicable, la dlgation est lune des stratgies les plus efcaces pour crer des classes thread-safe : il suft de laisser les classes thread-safe existantes grer lintgralit de ltat. Les bibliothques de la plate-forme Java contiennent un large ensemble de briques de base pour les applications concurrentes, comme les collections thread-safe et divers synchronisateurs qui permettent de coordonner le ux de contrle des threads qui cooprent lexcution. Nous prsenterons ici les plus utiles, notamment celles qui ont t introduites par Java 5.0 et Java 6, et nous montrerons quelques patrons dutilisation qui permettent de structurer les applications concurrentes.

5.1

Collections synchronises

Les classes collections synchronises incluent Vector et Hashtable, qui taient dj l dans le premier JDK, ainsi que leurs cousines ajoutes dans le JDK 1.2, les classes enveloppes synchronises que lon cre par les mthodes fabriques Collections .synchronizedXxx(). Ces classes ralisent la thread safety en encapsulant leur tat et en synchronisant chaque mthode publique de sorte quun seul thread puisse accder ltat de la collection un instant donn. 5.1.1 Problmes avec les collections synchronises

Bien que les collections synchronises soient thread-safe, il faut parfois utiliser un verrouillage ct client pour protger les actions composes comme les itrations (rcupration rpte de tous les lments de la collection), les navigations (rechercher un lment selon un certain ordre) et les oprations conditionnelles de type ajouter-siabsent (vrier quune cl K dun objet Map a une valeur associe et ajouter lassociation

84

Les bases

Partie I

(K, V) dans le cas contraire). Avec une collection synchronise, ces actions composes sont techniquement thread-safe, mme sans verrouillage ct client, mais elles peuvent ne pas se comporter comme vous vous y attendez lorsque dautres threads peuvent en mme temps modier la collection.

Le Listing 5.1 prsente deux mthodes portant sur un Vector, getLast() et deleteLast(), qui sont toutes les deux des squences tester-puis-agir. Chaque appel dtermine la taille du tableau et utilise ce rsultat pour rcuprer ou supprimer son dernier lment.
Listing 5.1 : Actions composes sur un Vector pouvant produire des rsultats inattendus.
public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex); }

Ces mthodes semblent inoffensives et le sont dans un certain sens : elles ne peuvent pas abmer le Vector, quel que soit le nombre de threads qui les appellent simultanment. Mais le code qui appelle ces mthodes peut avoir une opinion diffrente. Si un thread A appelle getLast() sur un Vector de dix lments, quun thread B appelle deleteLast() sur le mme Vector et que ces oprations soient entrelaces comme la Figure 5.1, getLast() lancera ArrayIndexOutOfBoundsException. Entre lappel size() et celui de get() dans getLast(), le Vector sest rtrci et lindice calcul lors de la premire tape nest donc plus valide. Le comportement est parfaitement cohrent avec la spcication de Vector une exception est leve lorsque lon demande un lment qui nexiste pas mais ce nest pas ce quattendait celui qui a appel getLast(), mme en prsence dune modication concurrente (sauf, peut-tre, si le Vector tait vide). Les collections synchronises respectant une politique de synchronisation qui autorise le verrouillage ct client1, il est alors possible de crer de nouvelles oprations qui seront atomiques par rapport aux autres oprations de la collection du moment que lon connat le verrou utiliser. En effet, ces classes protgent chaque mthode avec un verrou portant sur lobjet collection lui-mme : en prenant ce verrou, comme dans le Listing 5.2, nous pouvons donc rendre getLast() et deleteLast() atomiques, ce qui garantit que la taille du Vector ne changera pas entre lappel size() et celui de get(). Le risque que la taille du Vector puisse changer entre un appel size() et lappel get() existe aussi lorsque lon parcourt les lments du Vector comme dans le Listing 5.3.

1. Ceci nest document que trs vaguement dans le Javadoc de Java 5.0, comme exemple didiome ditration correct.

Chapitre 5

Briques de base

85

Cet idiome ditration suppose en effet que les autres threads ne modieront pas le Vector entre les appels size() et get(). Dans un environnement monothread, cette supposition est tout fait correcte mais, si dautres threads peuvent modier le Vector de faon concurrente, cela posera des problmes. Comme prcdemment, lexception ArrayIndexOutOfBoundsException sera dclenche si un autre thread supprime un lment pendant que lon parcourt le Vector et que lentrelacement des oprations est dfavorable.
size() 10 size() 10 remove(9)

B A Figure 5.1

get(9)

Boum !

Entrelacement de getLast() et deleteLast() dclenchant ArrayIndexOutOfBoundsException.

Listing 5.2 : Actions composes sur Vector utilisant un verrouillage ct client.


public static Object getLast(Vector list) { synchronized(list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } } public static void deleteLast(Vector list) { synchronized(list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }

Listing 5.3 : Itration pouvant dclencher ArrayIndexOutOfBoundsException.


for (int i = 0; i < vector.size(); i++) doSomething(vector.get(i));

Le fait que litration du Listing 5.3 puisse lever une exception ne signie pas que Vector ne soit pas thread-safe : ltat du Vector est toujours valide et lexception est, en ralit, conforme la spcication. Cependant, quune opration aussi banale que la rcupration du dernier lment dune itration dclenche une exception est clairement un comportement indsirable. Ce problme peut tre rgl par un verrouillage ct client, au prix dun cot supplmentaire en terme dadaptabilit. En gardant le verrou du Vector pendant la dure de litration, comme dans le Listing 5.4, on empche les autres threads de le modier pendant quon le parcourt. Malheureusement, ceci empche galement les autres threads dy accder pendant ce laps de temps, ce qui dtriore la concurrence.

86

Les bases

Partie I

Listing 5.4 : Itration avec un verrouillage ct client.


synchronized(vector) { for (int i = 0; i < vector.size(); i++) doSomething(vector.get(i)); }

5.1.2

Itrateurs et ConcurrentModificationException

Dans de nombreux exemples, nous utilisons Vector pour des raisons de simplicit, bien quelle soit considre comme une classe collection "historique". Cela dit, les classes plus "modernes" nliminent pas le problme des actions composes. La mthode standard pour parcourir une Collection consiste utiliser un Iterator, soit explicitement, soit via la syntaxe de la boucle "pour-chaque" introduite par Java 5.0, mais lutilisation ditrateurs nempche pas quil faille verrouiller la collection pendant son parcours si dautres threads peuvent la modier en mme temps. Les itrateurs renvoys par les collections synchronises ne sont pas conus pour grer les modications concurrentes et ils chouent rapidement (on dit que ce sont des itrateurs fail-fast) : sils dtectent que la collection a t modie depuis le dpart de litration, ils lancent lexception non contrle ConcurrentModificationException. Ces itrateurs "fail-fast" sont conus non pour tre infaillibles mais pour capturer "de bonne foi" les erreurs de concurrence ; ce ne sont donc que des indicateurs prcoces des problmes de concurrence. Ils sont implments en associant un compteur de modication la collection : si ce compteur change au cours de litration, hasNext() ou next() lancent ConcurrentModificationException. Cependant, ce test tant effectu sans synchronisation, il existe un risque de voir une valeur obsolte du compteur, et litrateur peut alors ne pas raliser quune modication a eu lieu. Il sagit dun compromis dlibr pour rduire limpact de ce code de dtection sur les performances 1. Le Listing 5.5 parcourt une collection avec la syntaxe de la boucle pour-chaque. En interne, javac produit un code qui utilise un Iterator et appelle hasNext() et next() pour parcourir la liste. Comme pour le parcours dun Vector, il faut donc maintenir un verrou sur la collection pendant la dure de litration si lon veut viter le dclenchement de ConcurrentModificationException.
Listing 5.5 : Parcours dun objet List avec un Iterator.
List<Widget> widgetList = Collections.synchronizedList (new ArrayList<Widget>()); ... // Peut lever ConcurrentModificationException for (Widget w : widgetList) doSomething(w);

1. ConcurrentModificationException peut galement survenir dans un code monothread ; elle est leve lorsque des objets sont supprims directement de la collection plutt quavec Iterator.remove().

Chapitre 5

Briques de base

87

Il y a cependant plusieurs raisons pour lesquelles le verrouillage dune collection pendant la dure de son parcours nest pas souhaitable. Les autres threads qui ont besoin daccder la collection se bloqueront jusqu la n de litration et, si la collection est grande ou que lopration effectue sur chaque lment prenne un certain temps, ils peuvent attendre longtemps. En outre, si la collection est verrouille comme dans le Listing 5.4, doSomething() est appele sous le couvert dun verrou, ce qui peut provoquer un blocage dnitif (ou deadlock, voir Chapitre 10). Mme en labsence de risque de famine ou de deadlock, le verrouillage des collections pendant un temps non ngligeable peut gner ladaptabilit de lapplication. Plus le verrou est dtenu longtemps, plus il sera disput et, si de nombreux threads se bloquent en attente de la disponibilit dun verrou, lutilisation du processeur peut en ptir (voir Chapitre 11). Au lieu de verrouiller la collection pendant litration, on peut cloner la collection et itrer sur la copie. Le clone tant conn au thread, aucun autre ne pourra le modier pendant son parcours, ce qui limine le risque de lexception ConcurrentModification Exception (mais il faut quand mme verrouiller la collection pendant lopration de clonage). Cloner une collection a videmment un cot en termes de performances ; que ce soit un compromis acceptable ou non dpend de nombreux facteurs, dont la taille de la collection, le type de traitement effectuer sur chaque lment, la frquence relative de litration par rapport aux autres oprations sur la collection et les exigences en termes de ractivit et de dbit des donnes. 5.1.3 Itrateurs cachs

Bien que le verrouillage puisse empcher les itrateurs de lancer ConcurrentModification Exception, vous devez penser lutiliser chaque fois quune collection partage peut tre parcourue par itration. Ceci est plus difcile quil ny parat car les itrateurs sont parfois cachs, comme dans la classe HiddenIterator du Listing 5.6. Cette classe ne contient aucune itration explicite, mais le code en gras revient en faire une. En effet, la concatnation des chanes est traduite par le compilateur en un appel StringBuilder .append(Object) , qui, son tour, appelle la mthode toString() de la collection, or limplmentation de toString() dans les collections standard parcourt la collection par itration et appelle toString() sur chaque lment pour produire une reprsentation correctement formate du contenu de celle-ci. La mthode addTenThings() pourrait donc lancer ConcurrentModificationException puisque la collection est itre par toString() au cours de la prparation du message de dbogage. Le vritable problme, bien sr, est que HiddenIterator nest pas threadsafe ; son verrou devrait tre pris avant dutiliser set() dans lappel println(), mais les codes de dbogage et de journalisation ngligent souvent de le faire. La leon quil faut en tirer est que plus la distance est importante entre ltat et la synchronisation qui le protge, plus il est probable quon oubliera dutiliser une synchronisation adquate lorsquon accdera cet tat. Si HiddenIterator enveloppait lobjet

88

Les bases

Partie I

HashSet dans un synchronizedSet en encapsulant ainsi la synchronisation, ce type derreur ne pourrait pas survenir.
Tout comme lencapsulation de ltat dun objet facilite la prservation de ses invariants, lencapsulation de sa synchronisation facilite le respect de sa politique de synchronisation.

Listing 5.6 : Itration cache dans la concatnation des chanes. Ne le faites pas.
public class HiddenIterator { @GuardedBy("this") private final Set<Integer> set = new HashSet<Integer>(); public synchronized void add(Integer i) { set.add(i); } public synchronized void remove(Integer i) { set.remove(i); } public void addTenThings() { Random r = new Random(); for (int i = 0; i < 10; i++) add(r.nextInt()); System.out.println("DEBUG: added ten elements to " + set); } }

Litration est galement invoque indirectement par les mthodes hashCode() et equals() de la collection, qui peuvent tre appeles si la collection est utilise comme un lment ou une cl dune autre collection. De mme, les mthodes containsAll(), removeAll() et retainAll(), ainsi que les constructeurs qui prennent des collections en paramtre, parcourent galement la collection. Toutes ces utilisations indirectes de litration peuvent provoquer une exception ConcurrentModificationException.

5.2

Collections concurrentes

Java 5.0 amliore les collections synchronises en fournissant plusieurs classes collections concurrentes. Les collections synchronises ralisent leur thread safety en srialisant tous les accs ltat de la collection, ce qui donne une concurrence assez pauvre : quand plusieurs threads concourent pour obtenir le verrou de la collection, le dbit des donnes en souffre. Les collections concurrentes, en revanche, sont conues pour les accs concurrents partir de plusieurs threads. Java 5.0 ajoute ConcurrentHashMap pour remplacer les implmentations de Map reposant sur des hachages synchroniss et CopyOnWriteArray List pour remplacer les implmentations de List synchronises dans les cas o lopration prdominante est le parcours dune liste. La nouvelle interface ConcurrentMap, quant elle, ajoute le support des actions composes classiques, comme ajouter-siabsent, remplacer et suppression conditionnelle.

Chapitre 5

Briques de base

89

Le remplacement des collections synchronises par les collections concurrentes permet dajouter normment dadaptabilit avec peu de risques.

Java 5.0 ajoute galement deux nouveaux types collection, Queue et BlockingQueue. Un objet Queue est conu pour contenir temporairement un ensemble dlments pendant quils sont en attente de traitement. Plusieurs implmentations sont galement fournies, dont ConcurrentLinkedQueue, une le dattente classique, et PriorityQueue, une le (non concurrente) a priorits. Les oprations sur Queue ne sont pas bloquantes ; si la le est vide, une opration de lecture renverra null. Bien que lon puisse simuler le comportement de Queue avec List (en fait, LinkedList implmente galement Queue), les classes Queue ont t ajoutes car liminer la possibilit daccs direct de List permet dobtenir des implmentations concurrentes plus efcaces.
BlockingQueue tend Queue pour lui ajouter des oprations dinsertion et de lecture bloquantes. Si la le est vide, la rcupration dun lment se bloquera jusqu ce quune donne soit disponible et, si la le est pleine (dans le cas des les de tailles limites), une insertion se bloquera tant quil ny a pas demplacement libre. Les les bloquantes sont trs importantes dans les schmas producteur-consommateur et seront prsentes en dtail dans la section 5.3.

Tout comme ConcurrentHashMap est un remplacement concurrent pour les Map synchroniss, Java 6 ajoute ConcurrentSkipListMap et ConcurrentSkipListSet comme remplacements concurrents des SortedMap ou SortedSet synchroniss (comme TreeMap ou TreeSet envelopps par synchronizedMap). 5.2.1

ConcurrentHashMap

Les collections synchronises maintiennent un verrou pour la dure de chaque opration. Certaines, comme HashMap.get() ou List.contains(), peuvent impliquer plus de travail quon pourrait le penser : parcourir un hachage ou une liste pour trouver un objet spcique ncessite dappeler la mthode equals() (qui, elle-mme, peut impliquer un calcul assez complexe) sur un certain nombre dobjets candidats. Dans une collection de type hachage, si hashCode() ne rpartit pas bien les valeurs de hachage, les lments peuvent tre distribus ingalement : dans le pire des cas, une mauvaise fonction de hachage transformera un hachage en liste chane. Le parcours dune longue liste et lappel de equals() sur quelques lments ou sur tous les lments peut donc prendre un certain temps pendant lequel aucun autre thread ne pourra accder la collection.
ConcurrentHashMap est un hachage Map comme HashMap, sauf quelle utilise une stratgie de verrouillage entirement diffrente qui offre une meilleure concurrence et une adaptabilit suprieure. Au lieu de synchroniser chaque mthode sur un verrou commun, ce qui restreint laccs un seul thread la fois, elle utilise un mcanisme de verrouillage plus n, appel verrouillage partitionn (lock striping, voir la section 11.4.3) an

90

Les bases

Partie I

dautoriser un plus grand degr daccs partag. Un nombre quelconque de threads lecteurs peuvent accder simultanment au hachage, en mme temps que les crivains, et un nombre limit de threads crivains peuvent modier le hachage de faon concurrente. On obtient ainsi un dbit de donnes bien plus important lors des accs concurrents, moyennant une petite perte de performances pour les accs monothreads.
ConcurrentHashMap, ainsi que les autres collections concurrentes, amliore encore les classes collections synchronises en fournissant des itrateurs qui ne lancent pas ConcurrentModificationException : il nest donc plus ncessaire de verrouiller la collection pendant litration. Les itrateurs renvoys par ConcurrentHashMap sont faiblement cohrents au lieu dtre fail-fast. Un itrateur faiblement cohrent peut tolrer une modication concurrente, il parcourt les lments tels quils taient lorsque litrateur a t construit et peut (mais ce nest pas garanti) reter les modications apportes la collection aprs la construction de litrateur.

Comme avec toutes les amliorations, il a fallu toutefois faire quelques compromis. La smantique des mthodes qui agissent sur tout lobjet Map, comme size() et isEmpty(), a t lgrement affaiblie pour reter la nature concurrente de la collection. Le rsultat de size() pouvant tre obsolte au moment o il est calcul, cette mthode peut donc renvoyer une valeur approximative au lieu dun comptage exact. Bien que cela puisse sembler troublant au premier abord, les mthodes comme size() et isEmpty() sont, en ralit, bien moins utiles dans les environnements concurrents puisque ces valeurs sont des cibles mouvantes. Les exigences de ces oprations ont donc t affaiblies pour permettre doptimiser les performances des oprations plus importantes que sont get(), put(), containsKey() et remove(). La seule fonctionnalit offerte par les implmentations synchronises de Map qui ne se retrouve pas dans ConcurrentHashMap est la possibilit de verrouiller le hachage pour disposer dun accs exclusif. Avec Hashtable et synchronizedMap, la dtention du verrou de lobjet Map empche tout autre thread dy accder. Cela peut tre ncessaire dans des cas spciaux, comme lorsque lon souhaite ajouter plusieurs associations de faon atomique ou que lon veut parcourir plusieurs fois le hachage en voyant chaque fois les lments dans le mme ordre. Cependant, globalement, labsence de cette fonctionnalit est un compromis acceptable puisque les collections concurrentes sont censes changer leurs lments en permanence.
ConcurrentHashMap ayant de nombreux avantages et peu dinconvnients par rapport Hashtable ou synchronizedMap, vous avez tout intrt remplacer ces dernires par ConcurrentHashMap an de disposer dune plus grande adaptabilit. Ce nest que lorsquune application a besoin de verrouiller un hachage pour y accder de manire exclusive1 que ConcurrentHashMap nest pas la meilleure solution.
1. Ou si vous avez besoin des effets de bord de la synchronisation des implmentations synchronises de Map.

Chapitre 5

Briques de base

91

5.2.2

Oprations atomiques supplmentaires sur les Map

Un objet ConcurrentHashMap ne pouvant pas tre verrouill pour garantir un accs exclusif, nous ne pouvons pas utiliser le verrouillage ct client pour crer de nouvelles oprations atomiques comme ajouter-si-absent, comme nous lavons fait pour Vector dans la section 4.4.1. Cependant, un certain nombre doprations composes comme ajouter-si-absent, supprimer-si-gal et remplacer-si-gal sont implmentes sous forme doprations atomiques par linterface ConcurrentMap prsente dans le Listing 5.7. Si vous constatez que vous devez ajouter lune de ces fonctionnalits une implmentation existante de Map synchronise, il sagit srement dun signe indiquant que vous devriez utiliser ConcurrentMap la place.
Listing 5.7 : Interface ConcurrentMap.
public interface ConcurrentMap<K,V> extends Map<K,V> { // Insertion uniquement si K na pas de valeur associe V putIfAbsent(K key, V value); // Suppression uniquement si K est associe V boolean remove(K key, V value); // Remplacement de la valeur uniquement si K est associe oldValue boolean replace(K key, V oldValue, V newValue); // Remplacement de la valeur uniquement si K est associe une valeur V replace(K key, V newValue); }

5.2.3

CopyOnWriteArrayList

CopyOnWriteArrayList remplace les List synchronises et offre une meilleure gestion de la concurrence dans quelques situations classiques. Avec cette classe, il ny a plus besoin de verrouiller ou de copier une collection pendant les itrations. De la mme faon, CopyOnWriteArraySet remplace les Set synchroniss.

La thread safety des collections "copie lors de lcriture" vient du fait quil ny a pas besoin de synchronisation supplmentaire pour accder un objet non modiable dans les faits qui a t correctement publi. Ces classes implmentent la "mutabilit" en crant et en republiant une nouvelle copie de la collection chaque fois quelle est modie. Les itrateurs de ces collections stockent une rfrence au tableau sous-jacent tel quil tait au dbut de litration et, comme il ne changera jamais, ils doivent ne se synchroniser que trs brivement pour assurer la visibilit de son contenu. Plusieurs threads peuvent donc parcourir la collection sans interfrer les uns avec les autres ni avec ceux qui veulent la modier. Les itrateurs renvoys par ces collections ne lvent pas ConcurrentModificationException et renvoient les lments tels quils taient la cration de litrateur, quelles que soient les modications apportes ensuite. videmment, la copie du tableau sous-jacent chaque modication de la collection a un cot, notamment lorsque la collection est importante ; il ne faut utiliser les collections

92

Les bases

Partie I

"copie lors de lcriture" que lorsque les oprations ditrations sont bien plus frquentes que celles de modication. Ce critre sapplique en ralit aux nombreux systmes de notication dvnements : signaler un vnement ncessite de parcourir la liste des couteurs enregistrs et dappeler chacun deux. Or, dans la plupart des cas, enregistrer ou dsinscrire un couteur dvnement est une opration bien moins frquente que recevoir une notication dvnement (voir [CPJ 2.4.4] pour plus dinformations sur la "copie lors de lcriture").

5.3

Files bloquantes et patron producteur-consommateur

Les les bloquantes fournissent les mthodes bloquantes put() et take(), ainsi que leurs quivalents offer() et poll(), qui utilisent un dlai dexpiration. Si la le est pleine, put() se bloque jusqu ce quun emplacement soit disponible ; si la le est vide, take() se bloque jusqu ce quil y ait un lment disponible. Les les peuvent tre bornes ou non bornes ; les les non bornes ntant jamais pleines, un appel put() sur ce type de le ne sera donc jamais bloquant. Les les bloquantes reconnaissent le patron de conception "producteur-consommateur". Ce patron permet de sparer lidentication dun travail de son excution en plaant les tches raliser dans une liste " faire" qui sera traite plus tard au lieu de ltre immdiatement mesure quelles sont identies. Le patron producteur-consommateur simplie donc le dveloppement puisquil supprime les dpendances entre les classes producteurs et consommateurs ; il allge galement la gestion de la charge de travail en dcouplant les activits qui peuvent produire ou consommer des donnes des vitesses diffrentes ou variables. Dans une conception producteur-consommateur construite autour dune le bloquante, les producteurs placent les donnes dans la le ds quelles deviennent disponibles et les consommateurs les rcuprent dans cette le lorsquils sont prts effectuer les actions adquates. Les producteurs nont pas besoin de connatre ni lidentit, ni le nombre de consommateurs, ni mme sil y a dautres producteurs : leur seule tche consiste placer des donnes dans la le. De mme, les consommateurs nont pas besoin de savoir qui sont les producteurs ni do provient le travail. BlockingQueue simplie limplmentation de ce patron avec un nombre quelconque de producteurs et de consommateurs. Lune des applications les plus frquentes du modle producteur-consommateur est un pool de threads associ une le de tches ; ce patron est dailleurs intgr dans le framework dexcution de tches Executor, qui sera prsent aux Chapitres 6 et 8. Un exemple dapplication du patron producteur-consommateur est la division des tches entre deux personnes qui font la vaisselle : lune lave les plats et les pose sur lgouttoir, lautre prend les plats sur lgouttoir et les essuie. Dans ce schma, lgouttoir sert de le bloquante ; sil ny a aucun plat dessus, le consommateur attend quil y ait des plats essuyer et, lorsque lgouttoir est plein, le producteur doit sarrter de laver jusqu ce

Chapitre 5

Briques de base

93

quil y ait de la place. Cette analogie stend plusieurs producteurs (bien quil puisse y avoir un conit pour laccs lvier) et plusieurs consommateurs ; chaque personne ninteragit quavec lgouttoir. Personne na besoin de savoir quel est le nombre de producteurs ou de consommateurs ni qui a produit un lment particulier. Les tiquettes "producteur" et "consommateur" sont relatives ; une activit qui agit comme un consommateur dans un contexte peut trs bien agir comme un producteur dans un autre. Essuyer les plats "consomme" des plats propres et mouills et "produit" des plats propres et secs. Une troisime personne voulant aider les deux plongeurs pourrait retirer les plats essuys, auquel cas celui qui essuie serait la fois un consommateur et un producteur et il y aurait alors deux les des tches partages (chacune pouvant bloquer celui qui essuie en lempchant de continuer son travail). Les les bloquantes simplient le codage des consommateurs car take() se bloque jusqu ce quil y ait des donnes disponibles. Si les producteurs ne produisent pas assez vite pour occuper les consommateurs, ceux-ci peuvent simplement attendre que du travail arrive. Parfois, ce fonctionnement est tout fait acceptable (cest ce que fait une application serveur lorsquaucun client ne demande ses services) et, parfois, cela indique que le nombre de threads producteurs par rapport aux threads consommateurs doit tre ajust pour amliorer lutilisation (comme pour un robot web ou toute autre application qui a un travail inni raliser). Si les producteurs produisent toujours plus vite que les consommateurs ne peuvent traiter, lapplication risque de manquer de mmoire car les tches sajouteront sans n la le. L aussi, la nature bloquante de put() simplie beaucoup le codage des producteurs : si lon utilise une le borne, les producteurs seront bloqus lorsque cette le sera pleine, ce qui laissera le temps aux consommateurs de rattraper leur retard puisquun producteur bloqu ne peut pas gnrer dautre tche. Les les bloquantes fournissent galement une mthode offer() qui renvoie un code derreur si llment na pas pu tre ajout. Elle permet donc de mettre en place des politiques plus souples pour grer la surcharge en rduisant par exemple le nombre de threads producteurs ou en les ralentissant dune manire ou dune autre.
Les les bornes constituent un outil de gestion des ressources relativement puissant pour produire des applications ables : elles rendent les programmes plus rsistants la surcharge en ralentissant les activits qui menacent de produire plus de travail quon ne peut en traiter.

Bien que le patron producteur-consommateur permette de dcoupler les codes du producteur et du consommateur, leur comportement reste indirectement associ par la le des tches partage. Il est tentant de supposer que les consommateurs suivront toujours le rythme, ce qui vite de devoir placer des limites la taille des les de tches,

94

Les bases

Partie I

mais cette supposition vous amnerait rarchitecturer votre systme plus tard. Construisez la gestion des ressources ds le dbut de la conception en utilisant des les bloquantes : cest bien plus facile de le faire dabord que de faire machine arrire plus tard. Les les bloquantes facilitent cette gestion dans un grand nombre de situations mais, si elles sintgrent mal votre conception, vous pouvez construire dautres structures de donnes bloquantes laide de la classe Semaphore (voir la section 5.5.3). La bibliothque de classes contient plusieurs implmentations de BlockingQueue. LinkedBlockingQueue et ArrayBlockingQueue sont des les dattente FIFO, analogues LinkedList et ArrayList mais avec une meilleure concurrence que celle dune List synchronise. PriorityBlockingQueue est une le a priorits, ce qui est utile lorsque lon veut traiter des lments dans un autre ordre que celui de leur insertion dans la le. Tout comme les collections tries, PriorityBlockingQueue peut comparer les lments selon leur ordre naturel (sils implmentent Comparable) ou laide dun Comparator. La dernire implmentation de BlockingQueue, SynchronousQueue, nest en fait pas une le dattente du tout car elle ne gre aucun espace de stockage pour les lments placs dans la le. Au lieu de cela, elle maintient une liste des threads qui attendent dajouter et de supprimer un lment de la le. Dans notre analogie de la vaisselle, cela reviendrait ne pas avoir dgouttoir mais passer directement les plats lavs au scheur disponible suivant. Bien que cela semble tre une trange faon dimplmenter une le, cela permet de rduire le temps de latence associ au dplacement des donnes du producteur au consommateur car les tches sont traites directement (dans une le traditionnelle, les oprations dajout et de suppression doivent seffectuer en squence avant quune tche puisse tre traite). Ce traitement direct fournit galement plus dinformations au producteur sur ltat de la tche ; lorsquelle est prise en charge, le producteur sait quun consommateur en a pris la responsabilit au lieu dtre place quelque part dans la le (cest la mme diffrence quentre remettre un document en main propre un collgue et le placer dans sa bote aux lettres en esprant quil la relvera bientt). Un objet SynchronousQueue nayant pas de capacit de stockage, les mthodes put() et take() se bloqueront si un autre thread nest pas prt participer au traitement. Les les synchrones ne conviennent gnralement quaux situations o il y a sufsamment de consommateurs et o il y en a presque toujours un qui sera prt traiter la tche. 5.3.1 Exemple : indexation des disques

Les agents qui parcourent les disques durs locaux et les indexent pour acclrer les recherches ultrieures (comme Google Desktop et le service dindexation de Windows) sont de bons clients pour la dcomposition en producteurs et consommateurs. La classe FileCrawler du Listing 5.8 prsente une tche productrice qui recherche dans une arborescence de rpertoires les chiers correspondant un critre dindexation et place

Chapitre 5

Briques de base

95

leurs noms dans la le des tches ; la classe Indexer du Listing 5.8 montre la tche consommatrice qui extrait les noms de la le et les indexe. Le patron producteur-consommateur offre un moyen de dcomposer ce problme dindexation en threads an dobtenir des composants plus simples. La factorisation du parcours et de lindexation de chiers en activits distinctes produit un code plus lisible et mieux rutilisable quune activit monolithique qui soccuperait de tout ; chaque activit na quune seule tche raliser et la le bloquante gre tout le contrle de ux. Le patron producteur-consommateur a galement plusieurs avantages en termes de performances. Les producteurs et les consommateurs peuvent sexcuter en parallle ; si lun deux est li aux E/S et que lautre est li au processeur, leur excution concurrente produira un dbit global suprieur celui obtenu par leur excution squentielle. Si les activits du producteur et du consommateur sont paralllisables des degrs diffrents, un couplage trop fort risque de ramener ce paralllisme celui de lactivit la moins paralllisable. Le Listing 5.9 lance plusieurs FileCrawler et Indexer dans des threads diffrents. Tel que ce code est crit, le thread consommateur ne se terminera jamais, ce qui empchera le programme lui-mme de se terminer ; nous tudierons plusieurs techniques pour corriger ce problme au Chapitre 7. Bien que cet exemple utilise des threads grs explicitement, de nombreuses conceptions producteur-consommateur peuvent sexprimer laide du framework dexcution des tches Executor, qui utilise lui-mme le patron producteur-consommateur.
Listing 5.8 : Tches producteur et consommateur dans une application dindexation des fichiers.
public class FileCrawler implements Runnable { private final BlockingQueue <File> fileQueue; private final FileFilter fileFilter; private final File root; ... public void run() { try { crawl(root); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private void crawl(File root) throws InterruptedException { File[] entries = root.listFiles(fileFilter); if (entries != null) { for (File entry : entries) if (entry.isDirectory()) crawl(entry); else if (!alreadyIndexed (entry)) fileQueue.put(entry); } } }

96

Les bases

Partie I

Listing 5.8 : Tches producteur et consommateur dans une application dindexation des fichiers. (suite)
public class Indexer implements Runnable { private final BlockingQueue <File> queue; public Indexer(BlockingQueue<File> queue) { this.queue = queue; } public void run() { try { while (true) indexFile(queue.take()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }

Listing 5.9 : Lancement de lindexation.


public static void startIndexing(File[] roots) { BlockingQueue<File> queue = new LinkedBlockingQueue <File>(BOUND); FileFilter filter = new FileFilter() { public boolean accept(File file) { return true; } }; for (File root : roots) new Thread(new FileCrawler(queue, filter, root)).start(); for (int i = 0; i < N_CONSUMERS; i++) new Thread(new Indexer(queue)).start(); }

5.3.2

Connement en srie

Les implmentations des les bloquantes dans java.util.concurrent contiennent toutes une synchronisation interne sufsante pour publier correctement les objets dun thread producteur vers un thread consommateur. Pour les objets modiables, les conceptions producteur-consommateur et les les bloquantes facilitent le connement en srie pour transfrer lappartenance des objets des producteurs aux consommateurs. Un objet conn un thread appartient exclusivement un seul thread, mais cette proprit peut tre "transfre" en le publiant correctement de sorte quun seul autre thread y aura accs et en sassurant que le thread qui publie ny accdera pas aprs ce transfert. La publication correcte garantit que ltat de lobjet est visible par son nouveau propritaire et, le propritaire initial ne le touchant plus, que cet objet est dsormais conn au nouveau thread. Le nouveau propritaire peut alors le modier librement puisquil est le seul y avoir accs. Les pools dobjets exploitent le connement en srie en "prtant" un objet un thread qui le demande. Tant que le pool dispose dune synchronisation interne sufsante pour publier lobjet correctement et tant que les clients ne publient pas eux-mmes cet objet

Chapitre 5

Briques de base

97

ou ne lutilisent pas aprs lavoir rendu au pool, la proprit peut tre transfre en toute scurit dun thread un autre. On pourrait galement utiliser dautres mcanismes de publication pour transfrer la proprit dun objet modiable, mais il faut alors sassurer quun seul thread recevra lobjet transfr. Les les bloquantes facilitent cette opration mais, avec un petit peu plus de travail, cela pourrait galement tre fait en utilisant la mthode atomique remove() de ConcurrentMap ou la mthode compareAndSet() de AtomicReference. 5.3.3 Classe Deque et vol de tches

Java 6 ajoute deux autres types collection, Deque (que lon prononce "deck") et Blocking Deque, qui tend Queue et BlockingQueue. Un objet Deque est une le double entre qui garantit des insertions et des supressions efcaces ses deux extrmits. Ses implmentations sappellent ArrayDeque et LinkedBlockingDeque. Tout comme les les bloquantes se prtent parfaitement au patron producteur-consommateur, les les doubles sont parfaitement adaptes au patron vol de tche. Alors que le patron producteur-consommateur nutilise quune seule le de tches partage par tous les consommateurs, le patron vol de tche utilise une le double par consommateur. Un consommateur ayant puis toutes les tches de sa le peut voler une tche la n de la le double dun autre. Le vol de tche est plus adaptatif quune conception producteurconsommateur traditionnelle car, ici, les travailleurs ne rivalisent pas pour une le partage ; la plupart du temps, ils naccdent qu leur propre le double, ce qui rduit donc les rivalits. Lorsquun travailleur doit accder la le dun autre, il le fait partir de la n de celle-ci plutt que de son dbut, ce qui rduit encore les conits. Le vol de tche est bien adapt aux problmes dans lesquels les consommateurs sont galement des producteurs lorsque lexcution dune tche identie souvent une autre tche. Le traitement dune page par un robot web, par exemple, identie gnralement de nouvelles pages analyser. Les algorithmes de parcours de graphes, comme le marquage du tas par le ramasse-miettes, peuvent aisment tre parallliss laide du vol de tches. Lorsquun travailleur identie une nouvelle tche, il la place la n de sa propre le double (ou, dans le cas dun partage du travail, sur celle dun autre travailleur) ; lorsque sa le est vide, il recherche du travail la n de la le de quelquun dautre, ce qui garantit que chaque travailleur restera occup.

5.4

Mthodes bloquantes et interruptions

Les threads peuvent se bloquer, ou se mettre en pause, pour plusieurs raisons : ils peuvent attendre la n dune opration dE/S, attendre de pouvoir prendre un verrou, attendre de se rveiller dun appel Thread.sleep() ou attendre le rsultat dun calcul effectu par un autre thread. Lorsquun thread se bloque, il est gnralement suspendu et plac dans lun des tats correspondant au blocage des threads (BLOCKED, WAITING ou TIMED_WAITING).

98

Les bases

Partie I

La diffrence entre une opration bloquante et une opration classique qui met simplement longtemps se terminer est quun thread bloqu doit attendre un vnement quil ne contrle pas avant de pouvoir poursuivre lopration dE/S sest termine, le verrou est devenu disponible ou le calcul externe sest achev. Lorsque cet vnement externe survient, le thread est replac dans ltat RUNNABLE et redevient ligible pour laccs au processeur. Les mthodes put() et take() de BlockingQueue, comme un certain nombre dautres mthodes de la bibliothque comme Thread.sleep(), lancent lexception contrle InterruptedException. Lorsquune mthode annonce quelle peut lancer Interrupted Exception, elle indique quelle est bloquante et que, si elle est interrompue, elle fera son possible pour se dbloquer le plut tt possible. La classe Thread fournit la mthode interrupt() pour interrompre un thread ou savoir si un thread a t interrompu (chaque thread a une proprit boolenne reprsentant son tat dinterruption). Linterruption dun thread est un mcanisme coopratif : un thread ne peut pas forcer un autre arrter ce quil fait pour faire quelque chose dautre ; lorsque le thread A interrompt le thread B, A demande simplement que B arrte son traitement en cours lorsquil trouvera un point darrt judicieux et sil en a envie. Bien que lAPI ou la spcication du langage ne prcisent nulle part quoi peut servir une interruption dans une application, son utilisation la plus courante consiste annuler une activit. Les mthodes bloquantes qui rpondent aux interruptions facilitent lannulation point nomm des activits qui tournent sans n. Si vous appelez une mthode susceptible de lever une InterruptedException, cette mthode est galement bloquante et vous devez avoir un plan pour rpondre linterruption. Pour le code dune bibliothque, deux choix sont essentiellement possibles :
m

Propager lInterruptedException. Cest souvent la politique la plus raisonnable si vous pouvez le faire il suft de propager linterruption au code appelant. Cela peut impliquer de ne pas capturer InterruptedException ou de la capturer an de la relancer aprs avoir effectu quelques oprations spciques de nettoyage. Restaurer linterruption. Parfois, vous ne pouvez pas lancer InterruptedException (si votre code fait partie dun Runnable, par exemple). Dans ce cas, vous devez la capturer et restaurer ltat dinterruption en appelant interrupt() sur le thread courant, an que le code plus haut dans la pile des appels puisse voir quune interruption est survenue, comme on le montre dans le Listing 5.10.

Vous pouvez aller bien plus loin avec les interruptions, mais ces deux approches sufront la grande majorit des situations. Il est toutefois dconseill de capturer Interrupted Exception pour ne rien faire en rponse. En effet, cela empcherait le code situ plus haut dans la pile des appels de ragir puisquil ne saura jamais que le thread a t interrompu. La seule situation o labsorption dune interruption est acceptable est lorsque

Chapitre 5

Briques de base

99

lon tend Thread et que lon veut donc contrler tout le code situ plus haut dans la pile. Lannulation et les interruptions sont prsentes plus en dtail au Chapitre 7.
Listing 5.10 : Restauration de ltat dinterruption afin de ne pas absorber linterruption.
public class TaskRunnable implements Runnable { BlockingQueue<Task> queue; ... public void run() { try { processTask(queue.take()); } catch (InterruptedException e) { // Restauration de ltat dinterruption Thread.currentThread().interrupt(); } } }

5.5

Synchronisateurs

Les les bloquantes sont uniques parmi les classes collections : non seulement elles servent de conteneurs, mais elles permettent galement de coordonner le ux de contrle des threads producteur et consommateur puisque les mthodes take() et put() se bloquent jusqu ce que la le soit dans ltat dsir (non vide ou non pleine). Un synchronisateur est un objet qui coordonne le contrle de ux des threads en fonction de son tat. Les les bloquantes peuvent donc servir de synchronisateurs ; parmi les autres types, on peut galement citer les smaphores, les barrires et les loquets. Bien que la bibliothque standard contienne dj un certain nombre de classes synchronisatrices, vous pouvez crer les vtres partir des mcanismes dcrits dans le Chapitre 14 si elles ne correspondent pas vos besoins. Tous les synchronisateurs partagent certaines proprits structurelles : ils encapsulent un tat qui dtermine si les threads qui leur parviennent seront autoriss passer ou forcs dattendre, ils founissent des mthodes pour manipuler cet tat et dautres pour attendre que le synchronisateur soit dans ltat attendu. 5.5.1 Loquets

Un loquet est un synchronisateur permettant de retarder lexcution des threads tant quil nest pas dans son tat terminal [CPJ 3.4.2]. Un loquet agit comme une porte : tant quil nest pas dans son tat terminal, la porte est ferme et aucun thread ne peut passer ; dans son tat terminal, la porte souvre et tous les threads peuvent entrer. Quand un loquet est dans son tat terminal, il ne peut changer dtat et reste ouvert jamais. Les loquets peuvent donc servir garantir que certaines activits ne se produiront pas tant quune autre activit ne sest pas termine. Voici quelques exemples dapplication :
m

Garantir quun calcul ne sera pas lanc tant que les ressources dont il a besoin nont pas t initialises. Un simple loquet binaire ( deux tats) peut servir indiquer que

100

Les bases

Partie I

"la ressource R a t initialise" et toute activit ncessitant R devra attendre ce loquet avant de sexcuter.
m

Garantir quun service ne sera pas lanc tant que dautres services dont il dpend nont pas dmarr. Chaque service utilise un loquet binaire qui lui est associ ; lancer le service S implique dabord dattendre les loquets des autres services dont dpend S, puis de relcher le loquet de S aprs son lancement pour que les services qui dpendent de S puissent leur tour tre lancs. Attendre que toutes les parties impliques dans une activit, les joueurs dun jeu, par exemple, soient prtes. Dans ce cas, le loquet natteint son tat terminal que lorsque tous les joueurs sont prts.

CountDownLatch est une implmentation des loquets pouvant tre utilise dans chacune de ces situations ; elle permet un ou plusieurs threads dattendre quun ensemble dvnements se produisent. Ltat du loquet est form dun compteur initialis avec un nombre positif qui reprsente le nombre dvnements attendre. La mthode count Down() dcrmente ce compteur pour indiquer quun vnement est survenu, tandis que la mthode await() attend que le compteur passe zro, ce qui signie que tous les vnements se sont passs. Si le compteur est non nul lorsquelle est appele, await() se bloque jusqu ce quil atteigne zro, que le thread appelant soit interrompu ou que le dlai dattente soit dpass.

La classe TestHarness du Listing 5.11 illustre deux utilisations frquentes des loquets. Elle cre un certain nombre de threads qui excutent en parallle une tche donne et utilise deux loquets, une "porte dentre" et une "porte de sortie". Le compteur de la porte dentre est initialis un, celui de la porte de sortie reoit le nombre de threads travailleurs. Chaque thread travailleur commence par attendre la porte dentre, ce qui garantit quaucun deux ne commencera travailler tant que tous les autres ne sont pas prts. la n de son excution, chaque thread dcrmente le compteur de la porte de sortie, ce qui permet au thread matre dattendre quils soient tous termins et de calculer le temps coul. Nous utilisons des loquets dans TestHarness au lieu de simplement lancer immdiatement les threads ds quils sont crs car nous voulons mesurer le temps ncessaire lexcution de la mme tche n fois en parallle. Si nous avions simplement cr et lanc les threads, les premiers auraient t "avantags" par rapport aux derniers et le degr de contention aurait vari au cours du temps, mesure que le nombre de threads actifs aurait augment ou diminu. Lutilisation dune porte dentre permet au thread matre de lancer simultanment tous les threads travailleurs et la porte de sortie lui permet dattendre que le dernier thread se termine au lieu dattendre que chacun deux se termine un un.

Chapitre 5

Briques de base

101

Listing 5.11 : Utilisation de la classe CountDownLatch pour lancer et stopper des threads et mesurer le temps dexcution.
public class TestHarness { public long timeTasks(int nThreads, final Runnable task) throws InterruptedException { final CountDownLatch startGate = new CountDownLatch (1); final CountDownLatch endGate = new CountDownLatch(nThreads); for (int i = 0; i < nThreads; i++) { Thread t = new Thread() { public void run() { try { startGate.await(); try { task.run(); } finally { endGate.countDown(); } } catch (InterruptedException ignored) { } } }; t.start(); } long start = System.nanoTime(); startGate.countDown(); endGate.await(); long end = System.nanoTime(); return end-start; } }

5.5.2

FutureTask

FutureTask agit galement comme un loquet (elle implmente Future, qui dcrit un calcul par paliers [CPJ 4.3.3]). Un calcul reprsent par un objet FutureTask est implment par un Callable, lquivalent par paliers de Runnable ; il peut tre dans lun des trois tats "attente dexcution", "en cours dexcution" ou "termin". La terminaison englobe toutes les faons par lesquelles un calcul peut se terminer, que ce soit une terminaison normale, une annulation ou une exception. Lorsquun objet FutureTask est dans ltat "termin", il y reste dnitivement.

Le comportement de Future.get() dpend de ltat de la tche. Si elle est termine, get() renvoie immdiatement le rsultat ; sinon elle se bloque jusqu ce que la tche passe dans ltat termin, puis renvoie le rsultat ou lance une exception. FutureTask transfre le rsultat du thread qui excute le calcul au(x) thread(s) qui le rcupre(nt) ; sa spcication garantit que ce transfert constitue une publication correcte du rsultat.
FutureTask est utilise par le framework Executor pour reprsenter les tches asynchrones et peut galement servir reprsenter tout calcul potentiellement long pouvant tre lanc avant que lon ait besoin du rsultat. La classe Preloader du Listing 5.12 lutilise pour effectuer un calcul coteux dont le rsultat sera ncessaire plus tard. En

102

Les bases

Partie I

lanant ce calcul assez tt, on rduit le temps quil faudra attendre ensuite, lorsquon aura vraiment besoin de son rsultat.
Listing 5.12 : Utilisation de FutureTask pour prcharger des donnes dont on aura besoin plus tard.
public class Preloader { private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(new Callable<ProductInfo>() { public ProductInfo call() throws DataLoadException { return loadProductInfo (); } }); private final Thread thread = new Thread(future); public void start() { thread.start(); } public ProductInfo get() throws DataLoadException , InterruptedException { try { return future.get(); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof DataLoadException) throw (DataLoadException) cause; else throw launderThrowable(cause); } } }

La classe Preloader cre un objet FutureTask qui dcrit la tche consistant charger les informations dun produit partir dune base de donnes et un thread qui effectuera le calcul. Elle fournit une mthode start() pour lancer ce thread car il est dconseill de le faire partir dun constructeur ou dun initialisateur statique. Lorsque le programme aura plus tard besoin du ProductInfo, il pourra appeler get(), qui renvoie les donnes charges si elles sont disponibles ou attend que le chargement soit termin sil ne lest pas encore. Les tches dcrites par Callable peuvent lancer des exceptions contrles ou non contrles et nimporte quel code peut lancer une instance de Error. Tout ce que lance le code de la tche est envelopp dans un objet ExecutionException et relanc partir de Future.get(). Cela complique le code qui appelle get(), non seulement parce quil doit grer la possibilit dune ExecutionException (et dune CancellationException non contrle), mais aussi parce que la raison de lExecutionException est renvoye sous forme de Throwable, ce qui est peu pratique grer. Lorsque get() lance une ExecutionException dans Preloader, la cause appartiendra lune des trois catgories suivantes : une exception contrle lance par lobjet Callable, une RuntimeException ou une Error. Nous devons traiter sparment ces trois cas mais nous utilisons la mthode auxiliaire launderThrowable() du Listing 5.13 pour encapsuler les parties les plus pnibles du code de gestion des exceptions. Avant dappeler cette mthode, Preloader teste les exceptions contrles connues et les relance : il ne

Chapitre 5

Briques de base

103

reste donc plus que les exceptions non contrles, que Preloader traite en les transmettant launderThrowable() et en lanant le rsultat renvoy par cet appel. Si lobjet Throwable pass launderThrowable() est une Error, la mthode la relance directement ; si ce nest pas une RuntimeException, elle lance une IllegalStateException pour indiquer une erreur de programmation. Il ne reste donc plus que RuntimeException, que launderThrowable() renvoie lappelant, qui, gnralement, se contentera de la relancer.
Listing 5.13 : Coercition dun objet Throwable non contrl en RuntimeException.
/** Si le Throwable est une Error, on le lance ; si cest une * RuntimeException, on le renvoie ; * sinon, on lance IllegalStateException */ public static RuntimeException launderThrowable(Throwable t) { if (t instanceof RuntimeException ) return (RuntimeException ) t; else if (t instanceof Error) throw (Error) t; else throw new IllegalStateException ("Not unchecked", t); }

5.5.3

Smaphores

Les smaphores servent contrler le nombre dactivits pouvant simultanment accder une ressource ou excuter une certaine action [CPJ 3.4.1]. Les smaphores permettent notamment dimplmenter des pools de ressources ou dimposer une limite une collection. Un objet Semaphore gre un ensemble de jetons virtuels, dont le nombre initial est pass au constructeur. Les activits peuvent prendre des jetons avec acquire() (sil en reste) et les rendre avec release() quand elles ont termin1. Sil ny a plus de jeton disponible, acquire() se bloque jusqu ce quil y en ait un (ou jusqu ce quelle soit interrompue ou que le dlai de lopration ait expir). Un smaphore binaire est un smaphore particulier puisque son nombre de jetons initial est gal un. Un smaphore binaire est souvent utilis comme mutex pour fournir une smantique de verrouillage non rentrante : celui qui dtient le seul jeton dtient le mutex. Les smaphores permettent dimplmenter des pools de ressources comme les pools de connexions aux bases de donnes. Bien quil soit relativement ais de mettre en place un pool de taille xe et de faire en sorte quune demande de ressource choue sil est
1. Limplmentation nutilise pas de vritables objets jetons et Semaphore nassocie pas aux threads les jetons distribus : un jeton pris par un thread peut tre rendu par un autre thread. Vous pouvez donc considrer acquire() comme une mthode qui consomme un jeton et release() comme une mthode qui en ajoute un ; un Semaphore nest pas limit au nombre de jetons qui lui ont t affects lors de sa cration.

104

Les bases

Partie I

vide, on prfre que cette demande se bloque si le pool est vide et se dbloque quand il ne lest plus. En initialisant un objet Semaphore avec un nombre de jetons gal la taille du pool, en prenant un jeton avant de tenter dobtenir une ressource et en redonnant le jeton aprs avoir remis la ressource dans le pool, acquire() se bloquera jusqu ce que le pool ne soit plus vide. Cette technique est utilise par la classe de tampon born du Chapitre 12 (un moyen plus simple de construire un pool bloquant serait dutiliser une BlockingQueue pour y stocker les ressources de ce pool). De mme, vous pouvez utiliser un Semaphore pour transformer nimporte quelle collection en collection borne et bloquante, comme on le fait dans la classe BoundedHashSet du Listing 5.14. Le smaphore est initialis avec un nombre de jetons gal la taille maximale dsire pour la collection et lopration add() prend un jeton avant dajouter un lment lensemble sous-jacent (si cette opration dajout sous-jacente najoute rien, on redonne immdiatement le jeton). Inversement, une opration de suppression russie redonne un jeton, permettant ainsi lajout dautres lments. Limplmentation Set sous-jacente ne sait rien de la limite xe, qui est gre par BoundedHashSet.
Listing 5.14 : Utilisation dun Semaphore pour borner une collection.
public class BoundedHashSet<T> { private final Set<T> set; private final Semaphore sem; public BoundedHashSet(int bound) { this.set = Collections.synchronizedSet(new HashSet<T>()); sem = new Semaphore(bound); } public boolean add(T o) throws InterruptedException { sem.acquire(); boolean wasAdded = false; try { wasAdded = set.add(o); return wasAdded; } finally { if (!wasAdded) sem.release(); } } public boolean remove(Object o) { boolean wasRemoved = set.remove(o); if (wasRemoved) sem.release(); return wasRemoved; } }

Chapitre 5

Briques de base

105

5.5.4

Barrires

Nous avons vu que les loquets facilitaient le dmarrage ou lattente de terminaison dun groupe dactivits apparentes. Les loquets sont des objets phmres : un loquet qui a atteint son tat terminal ne peut plus tre rinitialis. Les barrires ressemblent aux loquets car elles permettent de bloquer un groupe de threads jusqu ce quun vnement survienne [CPJ 4.4.3], mais la diffrence essentielle est quavec une barrire tous les threads doivent arriver en mme temps sur la barrire pour pouvoir sexcuter. Les loquets attendent donc des vnements tandis que les barrires attendent les autres threads. Une barrire implmente le protocole que certaines familles utilisent pour se donner rendez-vous : "Rendez-vous sur la place du Capitole 18 heures ; attendez que tout le monde arrive et nous verrons o aller ensuite."
CyclicBarrier permet un nombre donn de parties de se donner des rendez-vous rpts un point donn ; cette classe peut tre utilise par les algorithmes parallles itratifs qui dcomposent un problme en un nombre xe de sous-problmes indpendants. Les threads appellent await() lorsquils atteignent la barrire ; cet appel est bloquant tant que tous les threads nont pas atteint ce point. Lorsque tous les threads se sont rencontrs sur la barrire, celle-ci souvre et tous les threads sont librs. Elle se referme alors pour pouvoir tre rutilise. Si un appel await() dpasse son temps dexpiration ou si un thread bloqu par await() est interrompu, la barrire est considre comme brise et tous les appels await() en attente se terminent avec BrokenBarrierException. Si la barrire sest ouverte correctement, await() renvoie un indice darrive unique pour chaque thread, qui peut tre utilis pour "lire" un leader qui aura un rle particulier lors de litration suivante. CyclicBarrier permet galement de passser une action de barrire au constructeur, cest--dire un Runnable qui sera excut (dans lun des threads des sous-tches) lorsque la barrire se sera ouverte, mais avant que les threads bloqus soient librs.

On utilise souvent les barrires dans les simulations o le travail pour calculer une tape peut seffectuer en parallle mais que tout le travail associ une tape donne doit tre termin avant de passer ltape suivante. Dans les simulations de particules, par exemple, chaque tape calcule une nouvelle valeur pour la position de chaque particule en fonction des emplacements et des attributs des autres particules. Attendre sur une barrire entre chaque calcul garantit que toutes les modications pour ltape k se seront termines avant de passer ltape k + 1. La classe CellularAutomata du Listing 5.15 utilise une barrire pour effectuer une simulation de cellules, comme celle du jeu de la vie de Conway (Gardner, 1970). Lorsque lon paralllise une simulation, il est gnralement impossible daffecter un thread par lment (une cellule, dans le cas du jeu de la vie) : cela ncessiterait un trop grand nombre de threads et le surcot de leur coordination handicaperait les calculs. On choisit donc de partitionner le problme en un certain nombre de sous-parties, chacune tant prise en charge par un thread, et lon fusionne ensuite les rsultats. CellularAutomata

106

Les bases

Partie I

partitionne le damier en Ncpu parties, o Ncpu est le nombre de processeurs disponibles, et affecte chacune delles un thread1. chaque tape, les threads calculent les nouvelles valeurs pour toutes les cellules de leur partie du damier. Quand ils ont tous atteint la barrire, laction de barrire applique ces nouvelles valeurs au modle des donnes. Aprs cette action, les threads sont librs pour calculer ltape suivante, qui implique dappeler la mthode isDone() pour savoir si dautres itrations sont ncessaires.
Listing 5.15 : Coordination des calculs avec CyclicBarrier pour une simulation de cellules.
public class CellularAutomata { private final Board mainBoard; private final CyclicBarrier barrier; private final Worker[] workers; public CellularAutomata (Board board) { this.mainBoard = board; int count = Runtime.getRuntime().availableProcessors (); this.barrier = new CyclicBarrier (count, new Runnable() { public void run() { mainBoard.commitNewValues (); }}); this.workers = new Worker[count]; for (int i = 0; i < count; i++) workers[i] = new Worker(mainBoard.getSubBoard(count, i)); } private class Worker implements Runnable { private final Board board; public Worker(Board board) { this.board = board; } public void run() { while (!board.hasConverged()) { for (int x = 0; x < board.getMaxX(); x++) for (int y = 0; y < board.getMaxY(); y++) board.setNewValue(x, y, computeValue (x, y)); try { barrier.await(); } catch (InterruptedException ex) { return; } catch (BrokenBarrierException ex) { return; } } } } public void start() { for (int i = 0; i < workers.length; i++) new Thread(workers[i]).start(); mainBoard.waitForConvergence (); } }

1. Pour les traitements comme celui-ci, qui neffectuent pas dE/S et naccdent aucune donne partage, Ncpu ou Ncpu+1 threads fourniront le dbit optimal ; plus de threads namlioreront pas les performances et peuvent mme les dgrader car ils entreront en comptition pour laccs au processeur et la mmoire.

Chapitre 5

Briques de base

107

Exchanger est une autre forme de barrire, forme de deux parties qui changent des donnes [CPJ 3.4.3]. Les objets Exchanger sont utiles lorsque les parties effectuent des activits asymtriques : lorsquun thread remplit un tampon de donnes et que lautre les lit, par exemple. Ces threads peuvent alors utiliser un Exchanger pour se rencontrer et changer un tampon plein par un tampon vide. Lorsque deux threads changent des donnes via un Exchanger, lchange est une publication correcte des deux objets vers lautre partie.

Le timing de lchange dpend de la ractivit ncessaire pour lapplication. Lapproche la plus simple est que la tche qui remplit change lorsque le tampon est plein et que la tche qui vide change quand il est vide ; cela rduit au minimum le nombre dchanges, mais peut retarder le traitement des donnes si la vitesse darrive des nouvelles donnes nest pas prvisible. Une autre approche serait que la tche qui remplit change lorsque le tampon est plein mais galement lorsquil est partiellement rempli et quun certain temps sest coul.

5.6

Construction dun cache efcace et adaptable

Quasiment toutes les applications serveur utilisent une forme ou une autre de cache. La rutilisation des rsultats dun calcul prcdent peut en effet rduire les temps dattente et amliorer le dbit, au prix dun peu plus de mmoire utilise. Comme de nombreuses autres roues souvent rinventes, la mise en cache semble souvent plus simple quelle ne lest en ralit. Une implmentation nave dun cache transformera srement un problme de performances en problme dadaptabilit, mme si elle amliore les performances dune excution monothread. Dans cette section, nous allons dvelopper un cache efcace et adaptable pour y placer les rsultats dune fonction effectuant un calcul coteux. Commenons par lapproche vidente un simple HashMap et examinons quelques-uns de ses inconvnients en terme de concurrence et comment y remdier. Linterface Computable <A,V> du Listing 5.16 dcrit une fonction ayant une entre de type A et un rsultat de type V. La classe ExpensiveFunction, qui implmente Computable, met longtemps calculer son rsultat et nous aimerions crer une enveloppe Computable qui mmorise les rsultats des calculs prcdents en encapsulant le processus de mise en cache (cette technique est appele mmozation).
Listing 5.16 : Premire tentative de cache, utilisant HashMap et la synchronisation.
public interface Computable<A, V> { V compute(A arg) throws InterruptedException ; } public class ExpensiveFunction implements Computable<String, BigInteger> { public BigInteger compute(String arg) {

108

Les bases

Partie I

Listing 5.16 : Premire tentative de cache, utilisant HashMap et la synchronisation. (suite)


// Aprs une longue rflexion... return new BigInteger(arg); } } public class Memoizer1<A, V> implements Computable<A, V> { @GuardedBy("this") private final Map<A, V> cache = new HashMap<A, V>(); private final Computable<A, V> c; public Memoizer1(Computable<A, V> c) { this.c = c; } public synchronized V compute(A arg) throws InterruptedException { V result = cache.get(arg); if (result == null) { result = c.compute(arg); cache.put(arg, result); } return result; } }

A B C Figure 5.2

calcule f(1)

calcule f(2)

U renvoie le rsultat en cache de f(1)

Concurrence mdiocre de Memoizer1.

La classe Memoizer1 du Listing 5.16 montre notre premire tentative : on utilise un HashMap pour stocker les rsultats des calculs prcdents. La mthode compute() commence par vrier si le rsultat souhait se trouve dj dans le cache, auquel cas elle renvoie la valeur dj calcule. Sinon elle effectue le calcul et le stocke dans le HashMap avant de le renvoyer.
HashMap ntant pas thread-safe, Memoizer1 adopte lapproche prudente qui consiste synchroniser toute la mthode compute() pour sassurer que deux threads ne pourront pas accder simultanment au HashMap. Cela garantit la thread safety mais a un effet vident sur ladaptabilit puisquun seul thread peut excuter compute() la fois : si un autre thread est occup calculer un rsultat, les autres threads appelant cette mthode peuvent donc tre bloqus pendant un certain temps. Si plusieurs threads sont en attente pour calculer des valeurs qui nont pas encore t calcules, compute() peut, en fait,

Chapitre 5

Briques de base

109

mettre plus de temps sexcuter que si elle ntait pas mmose. La Figure 5.2 illustre ce qui pourrait se passer lorsque plusieurs threads tentent dutiliser une fonction mmose de cette faon. Ce nest pas ce genre damlioration des performances que nous esprions obtenir avec une mise en cache. La classe Memoizer2 du Listing 5.17 amliore la concurrence dsastreuse de Memoizer1 en remplaant le HashMap par un ConcurrentHashMap. Cette classe tant thread-safe, il ny a plus besoin de se synchroniser lorsque lon accde au hachage sous-jacent, ce qui limine la srialisation induite par la synchronisation de compute() dans Memoizer1.
Listing 5.17 : Remplacement de HashMap par ConcurrentHashMap.
public class Memoizer2<A, V> implements Computable<A, V> { private final Map<A, V> cache = new ConcurrentHashMap<A, V>(); private final Computable<A, V> c; public Memoizer2(Computable<A, V> c) { this.c = c; } public V compute(A arg) throws InterruptedException { V result = cache.get(arg); if (result == null) { result = c.compute(arg); cache.put(arg, result); } return result; } }

Memoizer2 offre certainement une meilleure concurrence que Memoizer1 : plusieurs threads peuvent lutiliser simultanment. Pourtant, elle souffre encore de quelques dfauts : deux threads appelant compute() au mme moment pourraient calculer la mme valeur. Pour une mmosation, cest un comportement assez inefcace puisque le but dun cache est justement dempcher de rpter plusieurs fois le mme calcul. Cest encore pire pour un mcanisme de cache plus gnral ; pour un cache dobjet cens ne fournir quune et une seule initialisation, cette vulnrabilit pose galement un risque de scurit.

Le problme avec Memoizer2 est que, lorsquun thread lance un long calcul, les autres threads ne savent pas que ce calcul est en cours et peuvent donc lancer le mme, comme le montre la Figure 5.3. Nous voudrions donc reprsenter le fait que "le thread X est en train de calculer f(27)" an quun autre thread voulant calculer f(27) sache que le moyen le plus efcace dobtenir ce rsultat consiste attendre que X ait ni et lui demande "quas-tu trouv pour f(27) ?". Nous avons dj rencontr une classe qui fait exactement cela : FutureTask. On rappelle que cette classe reprsente une tche de calcul qui peut, ou non, stre dj termine et que FutureTask.get() renvoie immdiatement le rsultat du calcul si celui-ci est disponible ou se bloque jusqu ce que le calcul soit termin puis renvoie son rsultat.

110

Les bases

Partie I

f(1) absent du cache

calcul de f(1) f(1) absent du cache

ajout de f(1) au cache calcul de f(1) ajout de f(1) au cache

Figure 5.3
Deux threads calculant la mme valeur avec Memoizer2.

La classe Memoizer3 du Listing 5.18 rednit donc le hachage sous-jacent comme un ConcurrentHashMap<A,Future<V>> au lieu dun ConcurrentHashMap<A,V>. Elle teste dabord si le calcul appropri a t lanc (et non termin comme dans Memoizer2). Si ce nest pas le cas, elle cre un objet FutureTask, lenregistre dans le hachage et lance le calcul ; sinon elle attend le rsultat du calcul en cours. Ce rsultat peut tre disponible immdiatement ou en cours de calcul, mais tout cela est transparent pour celui qui appelle Future.get().
Listing 5.18 : Enveloppe de mmosation utilisant FutureTask.
public class Memoizer3<A, V> implements Computable<A, V> { private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>(); private final Computable<A, V> c; public Memoizer3(Computable<A, V> c) { this.c = c; } public V compute(final A arg) throws InterruptedException { Future<V> f = cache.get(arg); if (f == null) { Callable<V> eval = new Callable<V>() { public V call() throws InterruptedException { return c.compute(arg); } }; FutureTask<V> ft = new FutureTask<V>(eval); f = ft; cache.put(arg, ft); ft.run(); // Lappel c.compute() a lieu ici } try { return f.get(); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } } }

Limplmentation de Memoizer3 est presque parfaite : elle produit une bonne concurrence (grce, essentiellement, lexcellente concurrence de ConcurrentHashMap), le rsultat est renvoy de faon efcace sil est dj connu et, si le calcul est en cours dans un autre thread, les autres threads qui arrivent attendent patiemment le rsultat. Elle na quun seul dfaut : il reste un risque que deux threads puissent calculer la mme valeur. Ce risque est bien moins lev quavec Memoizer2, mais le bloc if de compute() tant

Chapitre 5

Briques de base

111

toujours une squence tester-puis-agir non atomique, deux threads peuvent appeler compute() avec la mme valeur peu prs en mme temps, voir tous les deux que le cache ne contient pas la valeur dsire et donc lancer tous les deux le mme calcul. Ce timing malheureux est illustr par la Figure 5.4.
f(1) absent du cache f(1) absent du cache place Future pour f(1) dans le cache place Future pour f(1) dans le cache produit le rsultat produit le rsultat

calcule f(1)

B Figure 5.4

calcule f(1)

Timing malheureux forant Memoizer3 calculer deux fois la mme valeur.

Memoizer3 est vulnrable ce problme parce quune action compose (ajouter-siabsent) effectue sur le hachage sous-jacent ne peut pas tre rendue atomique avec le verrouillage. La classe Memoizer du Listing 5.19 rsout ce problme en tirant parti de la mthode atomique putIfAbsent() de ConcurrentMap.
Listing 5.19 : Implmentation finale de Memoizer.
public class Memoizer<A, V> implements Computable<A, V> { private final ConcurrentMap <A, Future<V>> cache = new ConcurrentHashMap <A, Future<V>>(); private final Computable<A, V> c; public Memoizer(Computable<A, V> c) { this.c = c; } public V compute(final A arg) throws InterruptedException { while (true) { Future<V> f = cache.get(arg); if (f == null) { Callable<V> eval = new Callable<V>() { public V call() throws InterruptedException { return c.compute(arg); } }; FutureTask<V> ft = new FutureTask<V>(eval); f = cache.putIfAbsent(arg, ft); if (f == null) { f = ft; ft.run(); } } try { return f.get(); } catch (CancellationException e) { cache.remove(arg, f); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } } } }

112

Les bases

Partie I

Mettre en cache un objet Future au lieu dune valeur fait courir le risque dune pollution du cache : si un calcul est annul ou choue, les futures tentatives de calculer ce rsultat indiqueront galement une annulation ou une erreur. Cest pour viter ce problme que Memoizer supprime lobjet Future du cache sil saperoit que le calcul a t annul ; il pourrait galement tre judicieux de faire de mme si lon dtecte une RuntimeException et que le calcul peut russir lors dune tentative extrieure. Memoizer ne gre pas non plus lexpiration du cache, mais cela pourrait se faire en utilisant une sous-classe de Future Task associant une date dexpiration chaque rsultat et en parcourant priodiquement le cache pour rechercher les entres expires (de mme, on ne traite pas lviction du cache consistant supprimer les anciennes entres an de librer de la place pour les nouvelles et ainsi empcher que le cache ne consomme trop despace mmoire). Avec cette implmentation concurrente dun cache dsormais complte, nous pouvons maintenant ajouter un vritable cache la servlet de factorisation du Chapitre 2, comme nous lavions promis. La classe Factorizer du Listing 5.20 utilise Memoizer pour mettre en cache de faon efcace et adaptative les valeurs dj calcules.
Listing 5.20 : Servlet de factorisation mettant en cache ses rsultats avec Memoizer.
@ThreadSafe public class Factorizer implements Servlet { private final Computable<BigInteger, BigInteger[]> c = new Computable<BigInteger, BigInteger[]>() { public BigInteger[] compute(BigInteger arg) { return factor(arg); } }; private final Computable<BigInteger, BigInteger[]> cache = new Memoizer<BigInteger, BigInteger[]>(c); public void service(ServletRequest req, ServletResponse resp) { try { BigInteger i = extractFromRequest (req); encodeIntoResponse (resp, cache.compute(i)); } catch (InterruptedException e) { encodeError(resp, "factorization interrupted"); } } }

Rsum de la premire partie


Nous avons dj prsent beaucoup de choses ! Lantische sur la concurrence qui suit rsume les concepts principaux et les rgles essentielles prsentes dans cette premire partie.1
Cest ltat modiable, idiot 1. Tous les problmes de concurrence se ramnent une coordination des accs ltat modiable. Moins ltat est modiable, plus il est facile dassurer la thread safety. Crez des champs final sauf sils ont besoin dtre modiables. Les objets non modiables sont automatiquement thread-safe. Les objets non modiables simplient normment la programmation concurrente. Ils sont plus simples et plus srs et peuvent tre partags librement sans ncessiter de verrous ni de copies dfensives. Lencapsulation permet de grer la complexit. Vous pourriez crire un programme thread-safe avec toutes les donnes dans des variables globales, mais pourquoi le faire ? Lencapsulation des donnes dans des objets facilite la prservation de leurs invariants ; lencapsulation de la synchronisation dans des objets facilite le respect de leur politique de synchronisation. Protgez chaque variable modiable par un verrou. Protgez toutes les variables dun invariant par le mme verrou. Gardez les verrous pendant lexcution des actions composes. Un programme qui accde une variable modiable partir de plusieurs threads et sans synchronisation est un programme faux. Ne croyez pas les raisonnements subtils qui vous expliquent pourquoi vous navez pas besoin de synchroniser. Ajoutez la thread safety la phase de conception, ou indiquez explicitement que votre classe nest pas thread-safe. Documentez votre politique de synchronisation.

1. En 1992, James Carville, lun des stratges de la victoire de Bill Clinton, avait afch au QG de campagne un pense-bte devenu lgendaire, Its the economy, stupid!, pour insister sur ce message lors de la campagne.

II
Structuration des applications concurrentes

6
Excution des tches
La plupart des applications concurrentes sont organises autour de lexcution de tches, que lon peut considrer comme des units de travail abstraites. Diviser une application en plusieurs tches simplie lorganisation du programme et facilite la dcouverte des erreurs grce aux frontires naturelles sparant les diffrentes transactions. Cette division encourage galement la concurrence en fournissant une structure naturelle permettant de parallliser le travail.

6.1

Excution des tches dans les threads

La premire tape pour organiser un programme autour de lexcution de tches consiste identier les frontires entre ces tches. Dans lidal, les tches sont des activits indpendantes, cest--dire des oprations qui ne dpendent ni de ltat, ni du rsultat, ni des effets de bord des autres tches. Cette indpendance facilite la concurrence puisque les tches indpendantes peuvent sexcuter en parallle si lon dispose des ressources de traitement adquates. Pour disposer de plus de souplesse dans lordonnancement et la rpartition de la charge entre ces tches, chacune devrait galement reprsenter une petite fraction des possibilits du traitement de lapplication. Les applications serveur doivent fournir un bon dbit de donnes et une ractivit correcte sous une charge normale. Les fournisseurs dapplications veulent des programmes permettant de supporter autant dutilisateurs que possible an de rduire dautant les cots par utilisateur ; ces utilisateurs veulent videmment obtenir rapidement les rponses quils demandent. En outre, les applications doivent non pas se dgrader brutalement lorsquelles sont surcharges mais ragir le mieux possible. Tous ces objectifs peuvent tre atteints en choisissant de bonnes frontires entre les tches et en utilisant une politique raisonnable dexcution des tches (voir la section 6.2.2). La plupart des applications serveur offrent un choix naturel pour les frontires entre tches : les diffrentes requtes des clients. Les serveurs web, de courrier, de chiers,

118

Structuration des applications concurrentes

Partie II

les conteneurs EJB et les serveurs de bases de donnes reoivent tous des requtes de clients distants via des connexions rseau. Utiliser ces diffrentes requtes comme des frontires de tches permet gnralement dobtenir la fois des tches indpendantes et de taille approprie. Le rsultat de la soumission dun message un serveur de courrier, par exemple, nest pas affect par les autres messages qui sont traits en mme temps, et la prise en charge dun simple message ne ncessite gnralement quun trs petit pourcentage de la capacit totale du serveur. 6.1.1 Excution squentielle des tches

Il existe un certain nombre de politiques possibles pour ordonnancer les tches au sein dune application ; parmi elles, certaines exploitent mieux la concurrence que dautres. La plus simple consiste excuter les tches squentiellement dans un seul thread. La classe SingleThreadWebServer du Listing 6.1 traite ses tches des requtes HTTP arrivant sur le port 80 en squence. Les dtails du traitement de la requte ne sont pas importants ; nous ne nous intressons ici qu la concurrence des diffrentes politiques dordonnancement.
Listing 6.1 : Serveur web squentiel.
class SingleThreadWebServer { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { Socket connection = socket.accept(); handleRequest(connection); } } }

SingleThreadedWebServer est simple et correcte dun point de vue thorique, mais serait trs inefcace en production car elle ne peut grer quune seule requte la fois. Le thread principal alterne constamment entre accepter des connexions et traiter la requte associe : pendant que le serveur traite une requte, les nouvelles connexions doivent attendre quil ait ni ce traitement et quil appelle nouveau accept(). Cela peut fonctionner si le traitement des requtes est sufsamment rapide pour que handle Request() se termine immdiatement, mais cette hypothse ne rete pas du tout la situation des serveurs web actuels.

Traiter une requte web implique un mlange de calculs et doprations dE/S. Le serveur doit lire dans un socket pour obtenir la requte et y crire pour envoyer la rponse ; ces oprations peuvent tre bloquantes en cas de congestion du rseau ou de problmes de connexion. Il peut galement effectuer des E/S sur des chiers ou lancer des requtes de bases de donnes, qui peuvent elles aussi tre bloquantes. Avec un serveur monothread, un blocage ne fait pas que retarder le traitement de la requte en cours : il empche galement celui des requtes en attente. Si une requte se bloque pendant un temps trs long, les utilisateurs penseront que le serveur nest plus disponible

Chapitre 6

Excution des tches

119

puisquil ne semble plus rpondre. En outre, les ressources sont mal utilises puisque le processeur reste inactif pendant que lunique thread attend que ses E/S se terminent. Pour les applications serveur, le traitement squentiel fournit rarement un bon dbit ou une ractivit correcte. Il existe des exceptions lorsquil y a trs peu de tches et quelles durent longtemps, ou quand le serveur ne sert quun seul client qui nenvoie quune seule requte la fois mais la plupart des applications serveur ne fonctionnent pas de cette faon1. 6.1.2 Cration explicite de threads pour les tches

Une approche plus ractive consiste crer un nouveau thread pour rpondre chaque nouvelle requte, comme dans le Listing 6.2.
Listing 6.2 : Serveur web lanant un thread par requte.
class ThreadPerTaskWebServer { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable task = new Runnable() { public void run() { handleRequest(connection); } }; new Thread(task).start(); } } }

La structure de la classe ThreadPerTaskWebServer ressemble celle de la version monothread le thread principal continue dalterner entre la rception dune connexion entrante et le traitement de la requte. Mais, ici, la boucle principale cre un nouveau thread pour chaque connexion an de traiter la requte au lieu de le faire dans le thread principal. Ceci a trois consquences importantes :
m

Le thread principal se dcharge du traitement de la tche, ce qui permet la boucle principale de poursuivre et de venir attendre plus rapidement la connexion entrante suivante. Ceci autorise de nouvelles connexions alors que les requtes sont en cours de traitement, ce qui amliore les temps de rponse. Les tches peuvent tre traites en parallle, ce qui permet de traiter simultanment plusieurs requtes. Ceci amliore le dbit lorsquil y a plusieurs processeurs ou si des tches doivent se bloquer en attente dune opration dE/S, de lacquisition dun verrou ou de la disponibilit dune ressource, par exemple.

1. Dans certaines situations, le traitement squentiel offre des avantages en termes de simplicit et de scurit ; la plupart des interfaces graphiques traitent squentiellement les tches dans un seul thread. Nous reviendrons sur le modle squentiel au Chapitre 9.

120

Structuration des applications concurrentes

Partie II

Le code du traitement de la tche doit tre thread-safe car il peut tre invoqu de faon concurrente par plusieurs tches.

En cas de charge modre, cette approche est plus efcace quune excution squentielle. Tant que la frquence darrive des requtes ne dpasse pas les capacits de traitement du serveur, elle offre une meilleure ractivit et un dbit suprieur. 6.1.3 Inconvnients dune cration illimite de threads

Cependant, dans un environnement de production, lapproche "un thread par tche" a quelques inconvnients pratiques, notamment lorsquelle peut produire un grand nombre de threads :
m

Surcot d au cycle de vie des threads. La cration dun thread et sa suppression ne sont pas gratuites. Bien que le surcot dpende des plates-formes, la cration dun thread prend du temps, induit une certaine latence dans le traitement de la requte et ncessite un traitement de la part de la JVM et du systme dexploitation. Si les requtes sont frquentes et lgres, comme dans la plupart des applications serveur, la cration dun thread par requte peut consommer un nombre non ngligeable de ressources. Consommation des ressources. Les threads actifs consomment des ressources du systme, notamment la mmoire. Sil y a plus de threads en cours dexcution quil ny a de processeurs disponibles, certains threads resteront inactifs, ce qui peut consommer beaucoup de mmoire et surcharger le ramasse-miettes. En outre, le fait que de nombreux threads concourent pour laccs aux processeurs peut galement avoir des rpercussions sur les performances. Si vous avez sufsamment de threads pour garder tous les processeurs occups, en crer plus namliorera rien, voire dgradera les performances. Stabilit. Il y a une limite sur le nombre de threads qui peuvent tre crs. Cette limite varie en fonction des plates-formes, des paramtres dappels de la JVM, de la taille de pile demande dans le constructeur de Thread et des limites imposes aux threads par le systme dexploitation1. Lorsque vous atteignerez cette limite, vous obtiendrez trs probablement une exception OutOfMemoryError. Tenter de se rtablir de cette erreur est trs risqu ; il est bien plus simple de structurer votre programme an dviter datteindre cette limite.

1. Sur des machines 32 bits, un facteur limitant important est lespace dadressage pour les piles des threads. Chaque thread gre deux piles dexcution : une pour le code Java, lautre pour le code natif. Gnralement, la JVM produit par dfaut une taille de pile combine denviron 512 kilo-octets (vous pouvez changer cette valeur avec le paramtre -Xss de la JVM ou lors de lappel du constructeur de Thread). Si vous divisez les 232 adresses par la taille de la pile de chaque thread, vous obtenez une limite de quelques milliers ou dizaines de milliers de threads. Dautres facteurs, comme les limites du systme dexploitation, peuvent imposer des limites plus contraignantes.

Chapitre 6

Excution des tches

121

Jusqu un certain point, ajouter plus de threads permet damliorer le dbit mais, audel de ce point, crer des threads supplmentaires ne fera que ralentir, et en crer un de trop peut totalement empcher lapplication de fonctionner. Le meilleur moyen de se protger de ce danger consiste xer une limite au nombre de threads quun programme peut crer et tester srieusement lapplication pour vrier quelle ne tombera pas court de ressources, mme lorsque cette limite sera atteinte. Le problme de lapproche "un thread par tche" est que la seule limite impose au nombre de threads crs est la frquence laquelle les clients distants peuvent lancer des requtes HTTP. Comme tous les autres problmes lis la concurrence, la cration innie de threads peut sembler fonctionner parfaitement au cours des phases de prototypage et de dveloppement, ce qui nempchera pas le problme dapparatre lorsque lapplication sera dploye en production et soumise une forte charge. Un utilisateur pervers, voire des utilisateurs ordinaires, peut donc faire planter votre serveur web en le soumettant une charge trop forte. Pour une application serveur suppose fournir une haute disponibilit et se dgrader correctement en cas de charge importante, il sagit dun srieux dfaut.

6.2

Le framework Executor

Les tches sont des units logiques de travail et les threads sont un mcanisme grce auquel ces tches peuvent sexcuter de faon asynchrone. Nous avons tudi deux politiques dexcution des tches laide des threads lexcution squentielle des tches dans un seul thread et lexcution de chaque tche dans son propre thread. Toutes les deux ont de svres limitations : lapproche squentielle implique une mauvaise ractivit et un faible dbit et lapproche "une tche par thread" souffre dune mauvaise gestion des ressources. Au Chapitre 5, nous avons vu comment utiliser des les de taille xe pour empcher quune application surcharge ne soit court de mmoire. Un pool de threads offrant les mmes avantages pour la gestion des threads, java.util.concurrent en fournit une implmentation dans le cadre du framework Executor. Comme le montre le Listing 6.3, la principale abstraction de lexcution des tches dans la bibliothque des classes Java est non pas Thread mais Executor.
Listing 6.3 : Interface Executor.
public interface Executor { void execute(Runnable command); }

Executor est peut-tre une interface simple, mais elle forme les fondements dun framework souple et puissant pour lexcution asynchrone des tches sous un grand nombre de politiques dexcution des tches. Elle fournit un moyen standard pour

122

Structuration des applications concurrentes

Partie II

dcoupler la soumission des tches de leur excution en les dcrivant comme des objets Runnable. Les implmentations de Executor fournissent galement un contrle du cycle de vie et des points dancrage permettant dajouter une collecte des statistiques, ainsi quune gestion et une surveillance des applications.
Executor repose sur le patron producteur-consommateur, o les activits qui soumettent des tches sont les producteurs (qui produisent le travail faire) et les threads qui excutent ces tches sont les consommateurs (qui consomment ce travail). Lutilisation dun Executor est, gnralement, le moyen le plus simple dimplmenter une conception de type producteur-consommateur dans une application.

6.2.1

Exemple : serveur web utilisant Executor

La cration dun serveur web partir dun Executor est trs simple. La classe Task ExecutionWebServer du Listing 6.4 remplace la cration des threads, qui tait code en dur dans la version prcdente, par un Executor. Ici, nous utilisons lune de ses implmentations standard, un pool de threads de taille xe, avec 100 threads. Dans TaskExecutionWebServer, la soumission de la tche de gestion dune requte est spare de son excution grce un Executor et son comportement peut tre modi en utilisant simplement une autre implmentation de Executor. Le changement dimplmentation ou de conguration dExecutor est une opration bien moins lourde que modier la faon dont les tches sont soumises ; gnralement, la conguration est un vnement unique qui peut aisment tre prsent lors du dploiement de lapplication, alors que le code de soumission des tches a tendance tre dissmin un peu partout dans le programme et tre plus difcile mettre en vidence.
Listing 6.4 : Serveur web utilisant un pool de threads.
class TaskExecutionWebServer { private static final int NTHREADS = 100; private static final Executor exec = Executors.newFixedThreadPool(NTHREADS); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable task = new Runnable() { public void run() { handleRequest(connection); } }; exec.execute(task); } } }

TaskExecutionWebServer peut tre facilement modi pour quil se comporte comme ThreadPerTaskWebServer : comme le montre le Listing 6.5, il suft dutiliser un Executor qui cre un nouveau thread pour chaque requte, ce qui est trs simple.

Chapitre 6

Excution des tches

123

Listing 6.5 : Executor lanant un nouveau thread pour chaque tche.


public class ThreadPerTaskExecutor implements Executor { public void execute(Runnable r) { new Thread(r).start(); }; }

De mme, il est tout aussi simple dcrire un Executor pour que TaskExecutionWeb Server se comporte comme la version monothread, en excutant chaque tche de faon synchrone dans execute(), comme le montre la classe WithinThreadExecutor du Listing 6.6.
Listing 6.6 : Executor excutant les tches de faon synchrone dans le thread appelant.
public class WithinThreadExecutor implements Executor { public void execute(Runnable r) { r.run(); }; }

6.2.2

Politiques dexcution

Lintrt de sparer la soumission de lexcution est que cela permet de spcier facilement, et donc de modier simplement, la politique dexcution dune classe de tches. Une politique dexcution rpond aux questions "quoi, o, quand et comment" concernant lexcution des tches :
m m m m m

Dans quel thread les tches sexcuteront-elles ? Dans quel ordre les tches devront-elles sexcuter (FIFO, LIFO, selon leurs priorits) ? Combien de tches peuvent sexcuter simultanment ? Combien de tches peuvent tre mises en attente dexcution ? Si une tche doit tre rejete parce que le systme est surcharg, quelle sera la victime et comment lapplication sera-t-elle prvenue ? Quelles actions faut-il faire avant ou aprs lexcution dune tche ?

Les politiques dexcution sont un outil de gestion des ressources et la politique optimale dpend des ressources de calcul disponibles et de la qualit du service recherche. En limitant le nombre de tches simultanes, vous pouvez garantir que lapplication nchouera pas si les ressources sont puises et que ses performances ne souffriront pas de problmes dus la concurrence pour des ressources en quantits limites 1.
1. Ceci est analogue lun des rles dun moniteur de transactions dans une application dentreprise ; il peut contrler la frquence laquelle les transactions peuvent tre traites, an de ne pas puiser des ressources limites.

124

Structuration des applications concurrentes

Partie II

Sparer la spcication de la politique dexcution de la soumission des tches permet de choisir une politique dexcution lors du dploiement adapte au matriel disponible.

chaque fois que vous voyez un code de la forme :


new Thread(runnable).start()

et que vous pensez avoir besoin dune politique dexcution plus souple, rchissez srieusement son remplacement par lutilisation dun Executor.

6.2.3

Pools de threads

Un pool de threads gre un ensemble homogne de threads travailleurs. Il est troitement li une le contenant les tches en attente dexcution. La vie des threads travailleurs est simple : demander la tche suivante dans la le, lexcuter et revenir attendre une autre tche. Lexcution des tches avec un pool de threads prsente un certain nombre davantages par rapport lapproche "un thread par tche". La rutilisation dun thread existant au lieu den crer un nouveau amortit les cots de cration et de suppression des threads en les rpartissant sur plusieurs requtes. En outre, le thread travailleur existant souvent dj lorsque la requte arrive, le temps de latence associ la cration du thread ne retarde pas lexcution de la tche, do une ractivit accrue. En choisissant soigneusement la taille du pool, vous pouvez avoir sufsamment de threads pour occuper les processeurs tout en vitant que lapplication soit court de mmoire ou se plante cause dune trop forte comptition pour les ressources. La bibliothque standard fournit une implmentation exible des pools de threads, ainsi que quelques congurations prdnies assez utiles. Pour crer un pool, vous pouvez appeler lune des mthodes fabriques statiques de la classe Executors :
m

newFixedThreadPool(). Cre un pool de taille xe qui cre les threads mesure que les tches sont soumises jusqu atteindre la taille maximale du pool, puis qui tente de garder constante la taille de ce pool (en crant un nouveau thread lorsquun thread meurt cause dune Exception inattendue). newCachedThreadPool(). Cre un pool de threads en cache, ce qui donne plus de souplesse pour supprimer les threads inactifs lorsque la taille courante du pool dpasse la demande de traitement et pour ajouter de nouveaux threads lorsque cette demande augmente, tout en ne xant pas de limite la taille du pool. newSingleThreadExecutor(). Cre une instance Executor monothread qui ne produit quun seul thread travailleur pour traiter les tches, en le remplaant sil meurt

Chapitre 6

Excution des tches

125

accidentellement. Les tches sont traites squentiellement selon lordre impos par la le dattente des tches (FIFO, LIFO, ordre des priorits)1.
m

newScheduledThreadPool(). Cre un pool de threads de taille xe, permettant de diffrer ou de rpter lexcution des tches, comme Timer (voir la section 6.2.5).

Les fabriques newFixedThreadPool() et newCachedThreadPool() renvoient des instances de la classe gnrale ThreadPoolExecutor qui peuvent galement servir directement construire des "excuteurs" plus spcialiss. Nous prsenterons plus en dtail les options de conguration des pools de threads au Chapitre 8. Le serveur web de TaskExecutionWebServer utilise un Executor avec un pool limit de threads travailleurs. Soumettre une tche avec execute() lajoute la le dattente dans laquelle les threads viennent sans cesse chercher des tches pour les excuter. Passer dune politique "un thread par tche" une politique utilisant un pool a un effet non ngligeable sur la stabilit de lapplication : le serveur web ne souffrira plus lorsquil sera soumis une charge importante2. En outre, son comportement se dgradera moins violemment puisquil ne cre pas des milliers de threads qui combattent pour des ressources processeur et mmoire limites. Enn, lutilisation dun Executor ouvre la porte toutes sortes dopportunits de conguration, de gestion, de surveillance, de journalisation, de suivi des erreurs et autres possibilits qui sont bien plus difciles ajouter sans un framework dexcution des tches. 6.2.4 Cycle de vie dun Executor

Nous avons vu comment crer un Executor mais pas comment larrter. Une implmentation de Executor cre des threads pour traiter des tches mais, la JVM ne pouvant pas se terminer tant que tous les threads (non dmons) ne se sont pas termins, ne pas arrter un Executor empche larrt de la JVM. Un Executor traitant les tches de faon asynchrone, ltat un instant donn des tches soumises nest pas vident. Certaines se sont peut-tre termines, certaines peuvent tre en cours dexcution et dautres peuvent tre en attente dexcution. Pour arrter une application, il y a une marge entre un arrt en douceur (nir ce qui a t lanc et ne pas accepter de nouveau travail) et un arrt brutal (teindre la machine), avec
1. Les instance Executor monothreads fournissent galement une synchronisation interne sufsante pour garantir que toute criture en mmoire par les tches sera visible par les tches suivantes ; ceci signie que les objets peuvent tre conns en toute scurit au "thread tche", mme si ce thread est remplac pisodiquement par un autre. 2. Mme sil ne souffrira plus cause de la cration dun nombre excessif de threads, il peut quand mme (bien que ce soit plus difcile) arriver court de mmoire si la frquence darrive des tches est suprieure celle de leur traitement pendant une priode sufsamment longue, cause de laugmentation de la taille de la le des Runnable en attente dexcution. Avec le framework Executor, ce problme peut se rsoudre en utilisant une le dattente de taille xe voir la section 8.3.2.

126

Structuration des applications concurrentes

Partie II

plusieurs degrs entre les deux. Les Executor fournissant un service aux applications, ils devraient galement pouvoir tre arrts, en douceur et brutalement, et renvoyer des informations lapplication sur ltat des tches affectes par cet arrt. Pour rsoudre le problme du cycle de vie du service dexcution, linterface Executor Service tend Executor en lui ajoutant un certain nombre de mthodes ddies la gestion du cycle de vie, prsentes dans le Listing 6.7 (elle ajoute galement certaines mthodes utilitaires pour la soumission des tches).
Listing 6.7 : Mthodes de ExecutorService pour le cycle de vie.
public interface ExecutorService extends Executor { void shutdown(); List<Runnable> shutdownNow(); boolean isShutdown(); boolean isTerminated(); boolean awaitTermination (long timeout, TimeUnit unit) throws InterruptedException ; // ... mthodes utilitaires pour la soumission des tches }

Le cycle de vie quimplique ExecutorService a trois tats : en cours dexcution, en cours darrt et termin. Les objets ExecutorService sont initialement crs dans ltat en cours dexcution. La mthode shutdown() lance un arrt en douceur : aucune nouvelle tche nest accepte, mais les tches dj soumises sont autorises se terminer mme celles qui nont pas encore commenc leur excution. La mthode shutdownNow() lance un arrt brutal : elle tente dannuler les tches en attente et ne lance aucune des tches qui sont dans la le et qui nont pas commenc. Les tches soumises un ExecutorService aprs son arrt sont gres par le gestionnaire dexcution rejete (voir la section 8.3.3), qui peut supprimer la tche sans prvenir ou forcer execute() lancer lexception non controle RejectedExecutionException. Lorsque toutes les tches se sont termines, lobjet ExecutorService passe dans ltat termin. Vous pouvez attendre quil atteigne cet tat en appelant la mthode await Termination() ou en linterrogeant avec isTerminated() pour savoir sil est termin. En gnral, on fait suivre immdiatement lappel shutdown() par awaitTermination(), an dobtenir leffet dun arrt synchrone de ExecutorService (larrt de Executor et lannulation de tche sont prsents plus en dtail au Chapitre 7). La classe LifecycleWebServer du Listing 6.8 ajoute un cycle de vie notre serveur web. Ce dernier peut dsormais tre arrt de deux faons : par programme en appelant stop() ou via une requte client en envoyant au serveur une requte HTTP respectant un certain format.
Listing 6.8 : Serveur web avec cycle de vie.
class LifecycleWebServer { private final ExecutorService exec = ...; public void start() throws IOException {

Chapitre 6

Excution des tches

127

ServerSocket socket = new ServerSocket(80); while (!exec.isShutdown()) { try { final Socket conn = socket.accept(); exec.execute(new Runnable() { public void run() { handleRequest(conn); } }); } catch (RejectedExecutionException e) { if (!exec.isShutdown()) log("task submission rejected", e); } } } public void stop() { exec.shutdown(); } void handleRequest (Socket connection) { Request req = readRequest(connection); if (isShutdownRequest(req)) stop(); else dispatchRequest(req); } }

6.2.5

Tches diffres et priodiques

La classe utilitaire Timer gre lexcution des tches diffres ("lancer cette tche dans 100 ms") et priodiques ("lancer cette tche toutes les 10 ms"). Cependant, elle a quelques inconvnients et il est prfrable dutiliser ScheduledThreadPoolExecutor la place1. Pour construire un objet ScheduledThreadPoolExecutor, on peut utiliser son constructeur ou la mthode fabrique newScheduledThreadPool(). Un Timer ne cre quun seul thread pour excuter les tches qui lui sont cones. Si lune de ces tches met trop de temps sexcuter, cela peut perturber la prcision du timing des autres TimerTask. Si une tche TimerTask est planie pour sexcuter toutes les 10 ms et quune autre TimerTask met 40 ms pour terminer son excution, par exemple, la tche rcurrente sera soit appele quatre fois de suite aprs la n de la tche longue soit "manquera" totalement quatre appels (selon quelle a t planie pour une frquence donne ou pour un dlai x). Les pools de thread rsolvent cette limitation en permettant dutiliser plusieurs threads pour excuter des tches diffres et planies. Un autre problme de Timer est son comportement mdiocre lorsquune TimerTask lance une exception non contrle : le thread Timer ne capturant pas lexception, une exception non contrle lance par une TimerTask met n au planicateur. En outre, dans cette situation, Timer ne ressuscite pas le thread : il suppose tort que lobjet Timer tout entier a t annul. En ce cas, les TimerTasks dj planies mais pas encore
1. Timer ne permet dordonnancer les tches que de faon absolue, pas relative, ce qui les rend dpendantes des modications de lhorloge systme ; ScheduledThreadPoolExecutor nutilise quun temps relatif.

128

Structuration des applications concurrentes

Partie II

excutes ne seront jamais lances et les nouvelles tches ne pourront pas tre planies (ce problme, appel "fuite de thread", est dcrit dans la section 7.3, en mme temps que les techniques permettant de lviter). La classe OutOfTime du Listing 6.9 illustre la faon dont un Timer peut tre perturb de cette manire et, comme un problme ne vient jamais seul, comment lobjet Timer partage cette confusion avec le malheureux client suivant qui tente de soumettre une nouvelle TimerTask. Vous pourriez vous attendre ce que ce programme sexcute pendant 6 secondes avant de se terminer alors quen fait il se terminera aprs 1 seconde avec une exception IllegalStateException associe au message "Timer already cancelled". ScheduledThreadPoolExecutor sachant correctement grer ces tches qui se comportent mal, il y a peu de raisons dutiliser Timer partir de Java 5.0.
Listing 6.9 : Classe illustrant le comportement confus de Timer.
public class OutOfTime { public static void main(String[] args) throws Exception { Timer timer = new Timer(); timer.schedule(new ThrowTask(), 1); SECONDS.sleep(1); timer.schedule(new ThrowTask(), 1); SECONDS.sleep(5); } static class ThrowTask extends TimerTask { public void run() { throw new RuntimeException(); } } }

Si vous devez mettre en place un service de planication, vous pouvez quand mme tirer parti de la bibliothque standard en utilisant DelayQueue, une implmentation de BlockingQueue fournissant les fonctionnalits de planication de ScheduledThread PoolExecutor. Un objet DelayQueue gre une collection dobjets Delayed, associs un dlai : DelayQueue ne vous autorise prendre un lment que si son dlai a expir. Les objets sortent dune DelayQueue dans lordre de leur dlai.

6.3

Trouver un paralllisme exploitable

Le framework Executor facilite la spcication dune politique dexcution mais, pour utiliser un Executor, vous devez dcrire votre tche sous la forme dun objet Runnable. Dans la plupart des applications serveur, le critre de sparation des tches est vident : cest une requte client. Parfois, dans la plupart des applications classiques notamment, il nest pas si facile de trouver une bonne sparation des tches. Mme dans les applications serveur, il peut galement exister un paralllisme exploitable dans une mme requte client ; cest parfois le cas avec les serveurs de bases de donnes (pour plus de dtails sur les forces en prsence lors du choix de la sparation des tches, voir [CPJ 4.4.1.1]).

Chapitre 6

Excution des tches

129

Dans cette section, nous allons dvelopper plusieurs versions dun composant permettant diffrents degrs de concurrence. Ce composant est la partie consacre au rendu dune page dans un navigateur : il prend une page HTML et la reprsente dans un tampon image. Pour simplier, nous supposerons que le code HTML ne contient que du texte balis, parsem dlments image ayant des dimensions et des URL prtablies. 6.3.1 Exemple : rendu squentiel dune page

Lapproche la plus simple consiste traiter squentiellement le document HTML. chaque fois que lon rencontre un marqueur, on le rend dans le tampon image ; lorsque lon rencontre un marqueur image, on rcupre limage sur le rseau et on la dessine galement dans le tampon. Cette technique est facile implmenter et ne ncessite quune seule manipulation de chaque lment dentre (il ny a mme pas besoin de mettre le document dans un tampon), mais elle risque dennuyer lutilisateur, qui devra attendre longtemps avant que tout le texte soit afch. Une approche moins ennuyeuse, mais toujours squentielle, consiste afcher dabord les lments textuels en laissant des emplacements rectangulaires pour les images puis, aprs avoir effectu cette premire passe, revenir tlcharger les images et les dessiner dans les rectangles qui leur sont associs. Cest ce que fait la classe SingleThreadRenderer du Listing 6.10.
Listing 6.10 : Affichage squentiel des lments dune page.
public class SingleThreadRenderer { void renderPage(CharSequence source) { renderText(source); List<ImageData> imageData = new ArrayList<ImageData>(); for (ImageInfo imageInfo : scanForImageInfo (source)) imageData.add(imageInfo.downloadImage()); for (ImageData data : imageData) renderImage(data); } }

Le tlchargement dune image implique dattendre la n dune opration dE/S pendant laquelle le processeur est peu actif. Lapproche squentielle risque donc de sous-employer le processeur et de faire attendre plus que ncessaire lutilisateur. Nous pouvons optimiser lutilisation du CPU et la ractivit du composant en dcoupant le problme en tches indpendantes qui pourront sexcuter en parallle. 6.3.2 Tches partielles : Callable et Future

Le framework Executor utilise Runnable comme reprsentation de base pour les tches. Runnable est une abstraction assez limite : run() ne peut pas renvoyer de valeur ni lancer dexception contrle, bien quelle puisse avoir des effets de bord comme crire dans un chier journal ou stocker un rsultat dans une structure de donnes partage. En pratique, de nombreuses tches sont des calculs diffrs excuter une requte sur une base de donnes, tlcharger une ressource sur le rseau ou calculer une fonction

130

Structuration des applications concurrentes

Partie II

complique. Pour ce genre de tche, Callable est une meilleure abstraction : son point dentre principal, call(), renverra une valeur et peut lancer une exception1. Executors contient plusieurs mthodes utilitaires permettant denvelopper dans un objet Callable dautres types de tches, comme Runnable et java.security.PrivilegedAction.
Runnable et Callable dcrivent des tches abstraites de calcul. Ces tches sont gnralement nies : elles ont un point de dpart bien dtermin et nissent par se terminer. Le cycle de vie dune tche excute par un Executor passe par quatre phases : cre, soumise, lance et termine. Les tches pouvant sexcuter pendant un temps assez long, nous voulons galement pouvoir annuler une tche. Dans le framework Executor, les tches qui ont t soumises mais pas encore lances peuvent toujours tre annules et celles qui ont t lances peuvent parfois tre annules si elles rpondent une interruption. Lannulation dune tche qui sest dj termine na aucun effet (lannulation sera prsente plus en dtail au Chapitre 7). Future reprsente le cycle de vie dune tche et fournit des mthodes pour tester si une tche sest termine ou a t excute, pour rcuprer son rsultat et pour annuler la tche. Les interfaces Callable et Future sont prsentes dans le Listing 6.11. La spcication de Future implique que le cycle de vie dune tche progresse toujours vers lavant, pas en arrire exactement comme le cycle de vie ExecutorService. Le comportement de get() dpend de ltat de la tche (pas encore lance, en cours dexcution ou termine). Cette mthode se termine immdiatement ou lance une Exception si la tche sest dj termine mais, dans le cas contraire, elle se bloque jusqu la n de la tche. Si la tche se termine en lanant une exception, get() relance lexception en lenveloppant dans ExecutionException ; si elle a t annule, get() lance Cancellation Exception. Si get() lance ExecutionException, lexception sous-jacente peut tre rcupre par getCause().
Listing 6.11 : Interfaces Callable et Future.
public interface Callable<V> { V call() throws Exception; } public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning ); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException , ExecutionException , CancellationException ; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, CancellationException , TimeoutException ; }

Il y a plusieurs moyens de crer un objet Future pour dcrire une tche. Les mthodes submit() de ExecutorService renvoyant toutes un Future, vous pouvez soumettre un
1. Pour indiquer quune tche ne renverra pas de valeur, on utilise Callable<Void>.

Chapitre 6

Excution des tches

131

objet Runnable ou Callable un Executor et rcuprer un Future utilisable pour rcuprer le rsultat ou annuler la tche. Vous pouvez galement instancier explicitement un objet FutureTask pour un Runnable ou un Callable donn (FutureTask implmentant Runnable, cet objet peut tre soumis un Executor pour quil lexcute ou excut directement en appelant sa mthode run()). partir de Java 6, les implmentations de ExecutorService peuvent rednir newTask For() dans AbstractExecutorService an de contrler linstanciation de lobjet Future correspondant un Callable ou un Runnable qui a t soumis. Comme le montre le Listing 6.12, limplmentation par dfaut se contente de crer un nouvel objet Future Task.
Listing 6.12 : Implmentation par dfaut de newTaskFor() dans ThreadPoolExecutor.
protected <T> RunnableFuture <T> newTaskFor(Callable<T> task) { return new FutureTask<T>(task); }

La soumission dun Runnable ou dun Callable un Executor constitue une publication correcte (voir la section 3.5) de ce Runnable ou de ce Callable, du thread qui le soumet au thread qui nira par excuter la tche. De mme, linitialisation de la valeur du rsultat pour un Future est une publication correcte de ce rsultat, du thread dans lequel il a t calcul vers tout thread qui le rcupre via get(). 6.3.3 Exemple : afchage dune page avec Future

La premire tape pour ajouter du paralllisme lafchage dune page consiste la diviser en deux tches, une pour afcher le texte, une autre pour tlcharger toutes les images (lune tant fortement lie au processeur et lautre, aux E/S, cette approche peut apporter une grosse amlioration, mme sur un systme monoprocesseur). Les classes Callable et Future peuvent nous aider exprimer linteraction entre ces tches coopratives. Dans le Listing 6.13, nous crons un objet Callable pour tlcharger toutes les images et nous le soumettons un ExecutorService an dobtenir un objet Future dcrivant lexcution de cette tche. Lorsque la tche principale a besoin des images, elle attend le rsultat en appelant Future.get(). Avec un peu de chance, ce rsultat sera dj prt lorsquon le demandera ; sinon nous aurons au moins lanc le tlchargement des images.
Listing 6.13 : Attente du tlchargement dimage avec Future.
public class FutureRenderer { private final ExecutorService executor = ...; void renderPage(CharSequence source) { final List<ImageInfo> imageInfos = scanForImageInfo (source); Callable<List<ImageData>> task = new Callable<List<ImageData>>() { public List<ImageData> call() {

132

Structuration des applications concurrentes

Partie II

Listing 6.13 : Attente du tlchargement dimage avec Future. (suite)


List<ImageData> result = new ArrayList<ImageData>(); for (ImageInfo imageInfo : imageInfos) result.add(imageInfo.downloadImage()); return result; } }; Future<List<ImageData>> future = executor.submit(task); renderText(source); try { List<ImageData> imageData = future.get(); for (ImageData data : imageData) renderImage(data); } catch (InterruptedException e) { // Raffirme ltat "interrompu" du thread Thread.currentThread().interrupt(); // On na pas besoin du rsultat, donc on annule aussi la tche future.cancel(true); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } } }

Le comportement de get() dpendant de ltat, lappelant na pas besoin de connatre ltat de la tche ; en outre, la publication correcte de la soumission de la tche et la rcupration du rsultat sufsent rendre cette approche thread-safe. Le code de gestion dexception associ lappel de Future.get() traite deux problmes possibles : la tche a pu rencontrer une exception ou le thread qui a appel get() a t interrompu avant que le rsultat ne soit disponible (voir les sections 5.5.2 et 5.4).
FutureRenderer permet dafcher le texte en mme temps que les images sont tlcharges. Lorsque toutes les images sont disponibles, elles sont afches sur la page. Cest une amlioration puisque lutilisateur voit rapidement le rsultat et on exploite ici le paralllisme, bien que lon puisse faire beaucoup mieux. En effet, lutilisateur na pas besoin dattendre que toutes les images soient tlcharges : il prfrerait srement les voir sparment mesure quelles deviennent disponibles.

6.3.4

Limitations du paralllisme de tches htrognes

Dans le dernier exemple, nous avons tent dexcuter en parallle deux types de tches diffrents tlcharger des images et afcher la page. Cependant, obtenir des gains de performances intressants en mettant en parallle des tches squentielles htrognes peut tre assez compliqu. Deux personnes peuvent diviser efcacement le travail qui consiste faire la vaisselle : lune lave pendant que lautre essuie. Cependant, affecter un type de tche diffrent chaque participant nest pas trs volutif : si plusieurs personnes proposent leur aide pour la vaisselle, il nest pas vident de savoir comment les rpartir sans quelles se gnent ou sans restructurer la division du travail. Si lon ne trouve pas un paralllisme sufsamment prcis entre des tches similaires, cette approche produira des rsultats dcevants.

Chapitre 6

Excution des tches

133

Un autre problme de la division des tches htrognes entre plusieurs participants est que ces tches peuvent avoir des tailles diffrentes. Si vous divisez les tches A et B entre deux participants, mais que A soit dix fois plus longue que B, nous naurez acclr le traitement total que de 9 %. Enn, diviser le travail entre plusieurs participants implique toujours un certain cot de coordination ; pour que cette division soit intressante, ce cot doit tre plus que compens par lamlioration de la productivit due au paralllisme.
FutureRenderer utilise deux tches : lune pour afcher le texte, lautre pour tlcharger les images. Si la premire est bien plus rapide que la seconde, comme cest srement le cas, les performances ne seront pas beaucoup diffrentes de celles de la version squentielle alors que le code sera bien plus compliqu. Le mieux que lon puisse obtenir avec deux threads est une vitesse multiplie par deux. Par consquent, tenter daugmenter la concurrence en mettant en parallle des activits htrognes peut demander beaucoup defforts et il y a une limite la dose de concurrence supplmentaire que vous pouvez en tirer (voir les sections 11.4.2 et 11.4.3 pour un autre exemple de ce phnomne).
Le vritable bnce en terme de performances de la division dun programme en tches sobtient lorsque lon peut traiter en parallle un grand nombre de tches indpendantes et homognes.

6.3.5

CompletionService : quand Executor rencontre BlockingQueue

Si vous avez un grand nombre de calculs soumettre un Executor et que vous vouliez rcuprer leurs rsultats ds quils sont disponibles, vous pouvez conserver les objets Future associs chaque tche et les interroger rgulirement pour savoir sils se sont termins en appelant leurs mthodes get() avec un dlai dexpiration de zro. Cest possible, mais ennuyeux. Heureusement, il y a une meilleure solution : un service de terminaison.
CompletionService combine les fonctionnalits dun Executor et dune BlockingQueue. Vous pouvez lui soumettre des tches Callable pour les excuter et utiliser des mthodes sur les les comme take() et poll() pour obtenir les rsultats calculs, empaquets sous la forme dobjets Future, ds quils sont disponibles. ExecutorCompletionService implmente CompletionService, en dlguant le calcul un Executor.

Limplmentation de ExecutorCompletionService est assez simple comprendre. Le constructeur cre une BlockingQueue qui contiendra les rsultats termins. FutureTask dispose dune mthode done() qui est appele lorsque le calcul sachve. Lorsquune tche est soumise, elle est enveloppe dans un objet QueueingFuture, une sous-classe de FutureTask qui rednit done() pour placer le rsultat dans la BlockingQueue, comme le montre le Listing 6.14. Les mthodes take() et poll() dlguent leur traitement la BlockingQueue et se bloquent si le rsultat nest pas encore disponible.

134

Structuration des applications concurrentes

Partie II

Listing 6.14 : La classe QueueingFuture utilise par ExecutorCompletionService.


private class QueueingFuture <V> extends FutureTask<V> { QueueingFuture(Callable<V> c) { super(c); } QueueingFuture(Runnable t, V r) { super(t, r); } protected void done() { completionQueue .add(this); } }

6.3.6

Exemple : afchage dune page avec CompletionService

CompletionService va nous permettre damliorer les performances de lafchage de la page de deux faons : un temps dexcution plus court et une meilleure ractivit. Nous pouvons crer une tche distincte pour tlcharger chaque image et les excuter dans un pool de threads, ce qui aura pour effet de transformer le tlchargement squentiel en tlchargement en parallle : cela rduit le temps ncessaire lobtention de toutes les images. En rcuprant les rsultats partir du CompletionService et en afchant chaque image ds quelle est disponible, nous fournissons lutilisateur une interface plus dynamique et plus ractive. Cette implmentation est dcrite dans le Listing 6.15.
Listing 6.15 : Utilisation de CompletionService pour afficher les lments de la page ds quils sont disponibles.
public class Renderer { private final ExecutorService executor; Renderer(ExecutorService executor) { this.executor = executor; } void renderPage(CharSequence source) { List<ImageInfo> info = scanForImageInfo (source); CompletionService <ImageData> completionService = new ExecutorCompletionService <ImageData>(executor); for (final ImageInfo imageInfo : info) completionService.submit( new Callable<ImageData>() { public ImageData call() { return imageInfo.downloadImage(); } }); renderText(source); try { for (int t = 0, n = info.size(); t < n; t++) { Future<ImageData> f = completionService.take(); ImageData imageData = f.get(); renderImage(imageData); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } } }

Chapitre 6

Excution des tches

135

Plusieurs ExecutorCompletionServices pouvant partager le mme Executor, il est tout fait sens de crer un ExecutorCompletionService priv un calcul particulier tout en partageant un Executor commun. Utilis de cette faon, un CompletionService agit comme un descripteur dun ensemble de calculs, exactement comme un Future agit comme un descripteur dun simple calcul. En mmorisant le nombre de tches soumises au CompletionService et en comptant le nombre de rsultats rcuprs, vous pouvez savoir quand tous les rsultats dun ensemble de calculs ont t obtenus, mme si vous utilisez un Executor partag. 6.3.7 Imposer des dlais aux tches

Le rsultat dune activit qui met trop de temps se terminer peut ne plus tre utile et lactivit peut alors tre abandonne. Une application web qui rcupre des publicits partir dun serveur externe, par exemple, pourrait afcher une publicit par dfaut si le serveur ne rpond pas au bout de 2 secondes an de ne pas dtriorer la ractivit du site. De la mme faon, un portail web peut rcuprer des donnes en parallle partir de plusieurs sources mais ne vouloir attendre quun certain temps avant dafcher les donnes. Le principal d de lexcution des tches dans un dlai imparti consiste sassurer que lon nattendra pas plus longtemps que ce dlai pour obtenir une rponse ou constater quune rponse nest pas fournie. La version temporise de Future.get() fournit cette garantie puisquelle se termine ds que le rsultat est disponible, mais lance Timeout Exception si le rsultat nest pas prt dans le dlai x. Le deuxime problme avec les tches avec dlais consiste les arrter lorsque le temps imparti sest coul an quelles ne consomment pas inutilement des ressources en continuant de calculer un rsultat qui, de toute faon, ne sera pas utilis. Pour cela, on peut faire en sorte que la tche gre de faon stricte son propre dlai et se termine delle-mme lorsquil est coul, ou lon peut annuler la tche lorsque le dlai a expir. Une nouvelle fois, Future peut nous aider ; si un appel get() temporis se termine avec une exception TimeoutException, nous pouvons annuler la tche via lobjet Future. Si la tche a t conue pour tre annulable (voir Chapitre 7), nous pouvons y mettre n prcocment an quelle ne consomme pas de ressources inutiles. Cest la technique utilise dans les Listings 6.13 et 6.16.
Listing 6.16 : Rcupration dune publicit dans un dlai imparti.
Page renderPageWithAd() throws InterruptedException { long endNanos = System.nanoTime() + TIME_BUDGET; Future<Ad> f = exec.submit(new FetchAdTask()); // Affiche la page pendant quon attend la publicit Page page = renderPageBody(); Ad ad; try { // On nattend que pendant le temps restant long timeLeft = endNanos - System.nanoTime();

136

Structuration des applications concurrentes

Partie II

Listing 6.16 : Rcupration dune publicit dans un dlai imparti. (suite)


ad = f.get(timeLeft, NANOSECONDS); } catch (ExecutionException e) { ad = DEFAULT_AD; } catch (TimeoutException e) { ad = DEFAULT_AD; f.cancel(true); } page.setAd(ad); return page; }

Ce dernier listing montre une application typique de la version temporise de Future.get(). Ce code produit une page web composite avec le contenu demand accompagn dune publicit rcupre partir dun serveur externe. La tche dobtention de la publicit est cone un Executor ; le code calcule le reste du contenu de la page puis attend la publicit jusqu lexpiration de son dlai1. Si celui-ci expire, il annule2 la tche de rcupration et utilise une publicit par dfaut. 6.3.8 Exemple : portail de rservations

Lapproche par dlai de la section prcdente peut aisment se gnraliser un nombre quelconque de tches. Dans un portail de rservation, par exemple, lutilisateur saisit des dates de voyages et le portail recherche et afche les tarifs dun certain nombre de vols, htels ou socits de location de vhicule. Selon la socit, la rcupration dun tarif peut impliquer lappel dun service web, la consultation dune base de donnes, lexcution dune transaction EDI ou tout autre mcanisme. Au lieu que le temps de rponse de la page ne soit dcid par la rponse la plus lente, il peut tre prfrable de ne prsenter que les informations disponibles dans un certain dlai. Dans le cas de fournisseurs ne rpondant pas temps, la page pourrait soit les omettre totalement, soit crire un texte comme "Air Java na pas rpondu temps". Obtenir un tarif auprs dune compagnie tant indpendant de lobtention des tarifs des autres compagnies, la rcupration dun tarif est une bonne candidate au dcoupage en tches, permettant ainsi lobtention en parallle des diffrents tarifs. Il serait assez simple de crer n tches, de les soumettre un pool de threads, de mmoriser les objets Future et dutiliser un appel get() temporis pour obtenir squentiellement chaque rsultat via son Future, mais il existe un moyen plus simple : invokeAll(). Le Listing 6.17 utilise la version temporise de invokeAll() pour soumettre plusieurs tches un ExecutorService et rcuprer les rsultats. La mthode invokeAll() prend
1. Le dlai pass get() est calcul en soustrayant lheure courante de la date limite ; on peut donc obtenir un nombre ngatif mais, toutes les mthodes temporises de java.util.concurrent traitant les dlais ngatifs comme des dlais nuls, aucun code supplmentaire nest ncessaire pour traiter ce cas. 2. Le paramtre true de Future.cancel() signie que le thread de la tche peut tre interrompu mme si la tche est en cours dexcution (voir Chapitre 7).

Chapitre 6

Excution des tches

137

en paramtre une collection de tches et renvoie une collection de Future. Ces deux collections ont des structures identiques ; invokeAll() ajoute les Future dans le rsultat selon lordre impos par litrateur de la collection des tches, ce qui permet lappelant dassocier un Future lobjet Callable quil reprsente. La version temporise de invokeAll() se terminera quand toutes les tches se seront termines, si le thread appelant est interrompu ou si le dlai imparti a expir. Toutes les tches non termines lexpiration du dlai sont annules. Au retour de invokeAll(), chaque tche se sera donc termine normalement ou aura t annule ; le code client peut appeler get() ou isCancelled() pour le savoir.
Listing 6.17 : Obtention de tarifs dans un dlai imparti.
private class QuoteTask implements Callable<TravelQuote> { private final TravelCompany company; private final TravelInfo travelInfo; ... public TravelQuote call() throws Exception { return company.solicitQuote(travelInfo); } } public List<TravelQuote> getRankedTravelQuotes ( TravelInfo travelInfo, Set<TravelCompany> companies, Comparator<TravelQuote> ranking, long time, TimeUnit unit) throws InterruptedException { List<QuoteTask> tasks = new ArrayList<QuoteTask>(); for (TravelCompany company : companies) tasks.add(new QuoteTask(company, travelInfo)); List<Future<TravelQuote>> futures = exec.invokeAll(tasks, time, unit); List<TravelQuote> quotes = new ArrayList<TravelQuote>(tasks.size()); Iterator<QuoteTask> taskIter = tasks.iterator(); for (Future<TravelQuote> f : futures) { QuoteTask task = taskIter.next(); try { quotes.add(f.get()); } catch (ExecutionException e) { quotes.add(task.getFailureQuote(e.getCause())); } catch (CancellationException e) { quotes.add(task.getTimeoutQuote (e)); } } Collections.sort(quotes, ranking); return quotes; }

Rsum
Structurer les applications autour de lexcution en tches permet de simplier le dveloppement et de faciliter le paralllisme. Grce au framework Executor, vous pouvez sparer la soumission des tches de la politique dexcution ; en outre, de nombreuses politiques

138

Structuration des applications concurrentes

Partie II

dexcution sont disponibles : chaque fois que vous devez crer des threads pour excuter des tches, pensez utiliser un Executor. Pour obtenir le maximum de bnce de la dcomposition dune application en tches, vous devez trouver comment sparer les tches. Dans certaines applications, cette dcomposition est vidente alors que, dans dautres, elle ncessite une analyse un peu plus pousse pour faire ressortir un paralllisme exploitable.

7
Annulation et arrt
Lancer des tches et des threads est une opration simple. La plupart du temps, on leur permet de dcider quand sarrter en les laissant sexcuter jusqu la n. Parfois, cependant, nous voulons stopper des tches plus tt que prvu, par exemple parce quun utilisateur a annul une opration ou parce que lapplication doit sarrter rapidement. Il nest pas toujours simple de stopper correctement, rapidement et de faon able des tches et des threads. Java ne fournit aucun mcanisme pour forcer en toute scurit un thread stopper ce quil tait en train de faire1. En revanche, il fournit les interruptions, un mcanisme coopratif qui permet un thread de demander un autre darrter ce quil est en train de faire. Lapproche cooprative est ncessaire car on souhaite rarement quune tche, un thread ou un service sarrte immdiatement puisquil pourrait laisser des structures de donnes partages dans un tat incohrent. Ces tches et services doivent donc tre cods pour que, lorsquon leur demande, ils nettoient le travail en cours puis se terminent. Cette pratique apporte une grande souplesse car le code lui-mme est gnralement plus quali que le code demandant lannulation pour effectuer le nettoyage ncessaire. Les problmes de n de vie peuvent compliquer la conception et limplmentation des tches, des services et des applications ; cest galement un aspect important de la conception des programmes qui est trop souvent ignor. Bien grer les pannes, les arrts et les annulations fait partie de ces caractristiques qui distinguent une application bien construite dune autre qui se contente de fonctionner. Ce chapitre prsente les mcanismes dannulation et dinterruption et montre comment coder les tches et les services pour quils rpondent aux demandes dannulation.
1. Les mthodes dprcies Thread.stop() et suspend() taient une tentative pour fournir ce mcanisme, mais on a vite ralis quelles avaient de srieux dfauts et quil fallait les viter. Pour une explication des problmes avec ces mthodes, consutez la page http://java.sun.com/ j2se/1.5.0/docs/ guide/misc/threadPrimitiveDeprecation.

140

Structuration des applications concurrentes

Partie II

7.1

Annulation des tches

Une activit est annulable, si un code externe peut lamener se terminer avant sa n normale. Il existe de nombreuses raisons dannuler une activit :
m

Annulation demande par lutilisateur. Celui-ci a cliqu sur le bouton "Annuler" dune interface graphique ou a demand lannulation via une interface de gestion comme JMX (Java Management Extensions). Activit limite dans le temps. Une application parcourt un espace de problmes pendant un temps ni et choisit la meilleure solution trouve pendant ce temps imparti. Quand le dlai expire, toutes les tches encore en train de chercher sont annules. vnement dune application. Une application parcourt un espace de problmes en le dcomposant pour que des tches distinctes examinent diffrentes rgions de cet espace. Lorsquune tche trouve une solution, toutes celles encore en train de chercher sont annules. Erreurs. Un robot web recherche des pages, les stocke ou produit un rsum des donnes sur disque. Sil rencontre une erreur (disque plein, par exemple), ses autres tches doivent tre annules, ventuellement aprs avoir enregistr leur tat courant an de pouvoir les reprendre plus tard. Arrt. Lorsque lon arrte une application ou un service, il faut faire quelque chose pour le travail en cours ou en attente de traitement. Dans un arrt en douceur, les tches en cours dexcution peuvent tre autorises se terminer alors quelle peuvent tre annules dans un arrt plus brutal.

Il ny a aucun moyen sr darrter autoritairement un thread en Java et donc aucun moyen sr darrter une tche. Il nexiste que des mcanismes coopratifs dans lesquels la tche et le code qui demande lannulation respectent un protocole dagrment. Lun de ces mcanismes consiste positionner un indicateur "demande dannulation" que la tche consultera priodiquement ; si elle constate quil est positionn, elle se termine ds que possible. Cest la technique quutilise la classe PrimeGenerator du Listing 7.1, qui numre les nombres premiers jusqu son annulation. La mthode cancel() positionne lindicateur dannulation qui est consult par la boucle principale avant chaque recherche dun nouveau nombre premier (pour que cela fonctionne correctement, lindicateur doit tre volatile).
Listing 7.1 : Utilisation dun champ volatile pour stocker ltat dannulation.
@ThreadSafe public class PrimeGenerator implements Runnable { @GuardedBy("this") private final List<BigInteger> primes = new ArrayList<BigInteger>(); private volatile boolean cancelled;

Chapitre 7

Annulation et arrt

141

public void run() { BigInteger p = BigInteger.ONE; while (!cancelled) { p = p.nextProbablePrime(); synchronized (this) { primes.add(p); } } } public void cancel() { cancelled = true; } public synchronized List<BigInteger> get() { return new ArrayList<BigInteger>(primes); } }

Le Listing 7.2 prsente un exemple dutilisation de cette classe dans lequel on laisse une seconde au gnrateur de nombres premiers avant de lannuler. Le gnrateur ne sarrtera pas ncessairement aprs exactement une seconde puisquil peut y avoir un lger dlai entre la demande dannulation et le moment o la boucle revient tester lindicateur. La mthode cancel() est appele partir dun bloc finally pour garantir que le gnrateur sera annul mme si lappel sleep() est interrompu. Si cancel() ntait pas appel, le thread de recherche des nombres premiers continuerait de sexcuter, ce qui consommerait des cycles processeur et empcherait la JVM de se terminer.
Listing 7.2 : Gnration de nombres premiers pendant une seconde.
List<BigInteger> aSecondOfPrimes() throws InterruptedException { PrimeGenerator generator = new PrimeGenerator(); new Thread(generator).start(); try { SECONDS.sleep(1); } finally { generator.cancel(); } return generator.get(); }

Pour tre annulable, une tche doit avoir une politique dannulation prcisant le "comment", le "quand" et le "quoi" de lannulation : comment un autre code peut demander lannulation, quand est-ce que la tche vrie quune annulation a t demande et quelles actions doit entreprendre la tche en rponse une demande dannulation. Prenons lexemple dune annulation dun chque : les banques ont des rgles qui prcisent la faon de demander lannulation dun paiement, les garanties sur leur ractivit pour une telle demande et les procdures qui suivront lannulation du chque (prvenir lautre banque implique dans la transaction et prlever des frais de dossier sur le compte du demandeur, par exemple). Prises ensemble, ces procdures et ces garanties forment la politique dannulation dun paiement par chque.
PrimeGenerator utilise une politique simple : le code client demande lannulation en appelant cancel(), le code principal teste les demandes dannulation pour chaque nombre premier et se termine lorsquil dtecte quune demande a eu lieu.

142

Structuration des applications concurrentes

Partie II

7.1.1

Interruption

Le mcanisme dannulation de PrimeGenerator nira par provoquer la n de la tche de calcul des nombres premiers, mais cela peut prendre un certain temps. Si une tche utilisant cette approche appelle une mthode bloquante comme BlockingQueue.put(), nous pourrions avoir un srieux problme : elle pourrait ne jamais tester lindicateur dannulation et donc ne jamais se terminer. Ce problme est illustr par la classe BrokenPrimeProducer du Listing 7.3. Le producteur gnre des nombres premiers et les place dans une le bloquante. Si le producteur va plus vite que le consommateur, la le se remplira et un appel put() sera bloquant. Que se passera-t-il si le consommateur essaie dannuler la tche productrice alors quelle est bloque dans put() ? Le consommateur peut appeler cancel(), qui positionnera lindicateur cancelled, mais le producteur ne le testera jamais car il ne sortira jamais de lappel put() bloquant (puisque le consommateur a cess de rcuprer des nombres premiers dans la le).
Listing 7.3 : Annulation non fiable pouvant bloquer les producteurs. Ne le faites pas.
class BrokenPrimeProducer extends Thread { private final BlockingQueue <BigInteger> queue; private volatile boolean cancelled = false; BrokenPrimeProducer (BlockingQueue <BigInteger> queue) { this.queue = queue; } public void run() { try { BigInteger p = BigInteger.ONE; while (!cancelled) queue.put(p = p.nextProbablePrime()); } catch (InterruptedException consumed) { } } public void cancel() { cancelled = true; } } void consumePrimes() throws InterruptedException { BlockingQueue<BigInteger> primes = ...; BrokenPrimeProducer producer = new BrokenPrimeProducer(primes); producer.start(); try { while (needMorePrimes()) consume(primes.take()); } finally { producer.cancel(); } }

Comme nous lavions voqu au Chapitre 5, certaines mthodes bloquantes de la bibliothque permettent dtre interrompues. Linterruption dun thread est un mcanisme coopratif permettant denvoyer un signal un thread pour quil arrte son traitement en cours et fasse autre chose, sil en a envie et quand il le souhaite.

Chapitre 7

Annulation et arrt

143

Rien dans lAPI ou les spcications du langage ne lie une interruption une smantique particulire de lannulation mais, en pratique, utiliser une interruption pour autre chose quune annulation est une dmarche fragile et difcile grer dans les applications de taille importante.

Chaque thread a un indicateur dinterruption boolen prcisant sil a t interrompu. Comme le montre le Listing 7.4, la classe Thread contient des mthodes permettant dinterrompre un thread et dinterroger cet indicateur. La mthode interrupt() interrompt le thread cible et isInterrupted() renvoie son tat dinterruption. La mthode statique interrupted() porte mal son nom puisquelle rinitialise lindicateur dinterruption du thread courant et renvoie sa valeur prcdente : cest le seul moyen de rinitialiser cet indicateur.
Listing 7.4 : Mthodes dinterruption de Thread.
public class Thread { public void interrupt() { ... } public boolean isInterrupted() { ... } public static boolean interrupted() { ... } ... }

Les mthodes bloquantes de la bibliothque, comme Thread.sleep() et Object.wait(), tentent de dtecter quand un thread a t interrompu, auquel cas elles se terminent plus tt que prvu. Elles rpondent linterruption en rinitialisant lindicateur dinterruption et en lanant InterruptedException pour indiquer que lopration bloquante sest termine prcocment cause dune interruption. La JVM ne garantit pas le temps que mettra une mthode bloquante pour dtecter une interruption mais, en pratique, cest relativement rapide. Si un thread est interrompu alors quil ntait pas bloqu, son indicateur dinterruption est positionn et cest lactivit annule de linterroger pour dtecter linterruption. En ce sens, linterruption est "collante" : si elle ne dclenche pas une InterruptedException, la trace de linterruption persiste jusqu ce que quelquun rinitialise dlibrment lindicateur.
Lappel interrupt() nempche pas ncessairement le thread cible de continuer ce quil est en train de faire : il indique simplement quune interruption a t demande.

En ralit, une interruption ninterrompt pas un thread en cours dexcution : elle ne fait que demander au thread de sinterrompre lui-mme la prochaine occasion (ces occasions sont appeles points dannulation). Certaines mthodes comme wait(), sleep() et join() prennent ces requtes au srieux et lvent une exception lorsquelles les reoivent ou rencontrent un indicateur dinterruption dj positionn. Dautres, pourtant tout

144

Structuration des applications concurrentes

Partie II

fait correctes, peuvent totalement ignorer ces requtes et les laisser en place pour que le code appelant puisse en faire quelque chose. Les mthodes mal conues perdent les requtes dinterruption, empchant ainsi le code situ plus haut dans la pile des appels dagir en consquence. La mthode statique interrupted() doit tre utilise avec prcaution puisquelle rinitialise lindicateur dinterruption du thread courant. Si vous lappelez et quelle renvoie true, vous devez agir ( moins que vous ne vouliez perdre linterruption) en lanant InterruptedException ou en restaurant lindicateur en rappelant interrupt() comme dans le Listing 5.10.
BrokenPrimeProducer montre que les mcanismes personnaliss dannulation ne font pas toujours bon mnage avec les mthodes bloquantes de la bibliothque. Si vous codez vos tches pour quelles rpondent aux interruptions, vous pouvez les utiliser comme mcanisme dannulation et tirer parti du support des interruptions fourni par de nombreuses classes de la bibliothque.
Une interruption est gnralement le meilleur moyen dimplmenter lannulation.

Comme le montre le Listing 7.5, on peut facilement corriger (et simplier) BrokenPrime Producer en utilisant une interruption la place dun indicateur boolen pour demander lannulation.
Listing 7.5 : Utilisation dune interruption pour lannulation.
class PrimeProducer extends Thread { private final BlockingQueue <BigInteger> queue; PrimeProducer(BlockingQueue <BigInteger> queue) { this.queue = queue; } public void run() { try { BigInteger p = BigInteger.ONE; while (!Thread.currentThread().isInterrupted()) queue.put(p = p.nextProbablePrime()); } catch (InterruptedException consumed) { /* Autorise le thread se terminer */ } } public void cancel() { interrupt(); } }

Linterruption peut tre dtecte deux endroits dans chaque itration de la boucle : dans lappel bloquant put() et en interrogeant explicitement lindicateur dinterruption dans le test de boucle. Ce test explicite nest dailleurs pas strictement ncessaire ici cause de lappel bloquant put() mais il rend PrimeProducer plus ractive une interruption puisquil teste linterruption avant de commencer la recherche dun nombre premier, ce qui peut tre une opration assez longue. Lorsque les appels aux mthodes

Chapitre 7

Annulation et arrt

145

bloquantes ne sont pas sufsamment frquents pour produire la ractivit attendue, un test explicite de lindicateur dinterruption peut amliorer la situation. 7.1.2 Politiques dinterruption

Tout comme les tches devraient avoir une politique dannulation, les threads devraient respecter une politique dinterruption. Une telle politique dtermine la faon dont un thread interprtera une demande dinterruption : ce quil fera (sil fait quelque chose) lorsquil en dtectera une, quelles units de travail seront considres comme atomiques par rapport linterruption et quelle vitesse il ragira cette interruption. La politique dinterruption la plus raisonnable est une forme dannulation au niveau du thread ou du service : quitter aussi vite que possible, nettoyer si ncessaire et, ventuellement, prvenir lentit propritaire que le thread se termine. On peut tablir dautres politiques comme mettre un service en pause ou le relancer mais, en ce cas, les threads ou les pools de threads utilisant une politique dinterruption non standard devront peuttre se limiter des tches crites pour tenir compte de cette politique. Il est important de faire la diffrence entre la faon dont les tches et les threads devraient ragir aux interruptions. Une simple requte dinterruption peut avoir dautres destinataires que celui qui est initialement vis : interrompre un thread dans un pool peut signier "annule la tche courante" et "termine ce thread". Les tches ne sexcutent pas dans des threads quelles possdent ; elles empruntent des threads qui appartiennent un service comme un pool de threads. Le code qui ne possde pas le thread (pour un pool de thread, il sagit du code situ lextrieur de limplmentation du pool) doit faire attention prserver lindicateur dinterruption an que le code propritaire puisse ventuellement y ragir, mme si le code "invit" ragit galement linterruption (lorsque lon garde une maison, on ne jette pas le courrier qui arrive pour les propritaires on le sauvegarde et on les laisse sen occuper leur retour, mme si on lit leurs magazines). Cest la raison pour laquelle la plupart des mthodes bloquantes de la bibliothque se contentent de lancer InterruptedException en rponse une interruption. Comme elles ne sexcuteront jamais dans un thread qui leur appartient, elles implmentent la politique dannulation la plus raisonnable pour une tche ou un code dune bibliothque : elles dbarrassent le plancher le plus vite possible et transmettent linterruption lappelant an que le code situ plus haut dans la pile des appels puisse prendre les mesures ncessaires. Une tche na pas ncessairement besoin de tout abandonner lorsquelle dtecte une demande dinterruption elle peut choisir un moment plus opportun en mmorisant quelle a t interrompue, en nissant le travail quelle effectuait puis en lanant Interrupted Exception ou tout autre signal indiquant linterruption. Cette technique permet dviter

146

Structuration des applications concurrentes

Partie II

quune interruption survenant au beau milieu dune modication nabme les structures de donnes. Une tche ne devrait jamais rien supposer sur la politique dinterruption du thread qui lexcute, sauf si elle a t explicitement conue pour sexcuter au sein dun service utilisant une politique dinterruption spcique. Quelle interprte une interrruption comme une annulation ou quelle agisse dune certaine faon en cas dinterruption, une tche devrait prendre soin de prserver lindicateur dinterruption du thread qui sexcute. Si elle ne se contente pas de propager InterruptedException son appelant, elle devrait restaurer lindicateur dinterruption aprs avoir captur lexception :
Thread.currentThread().interrupt();

Tout comme le code dune tche ne devrait pas faire de supposition sur ce que signie une interruption pour le thread qui lexcute, le code dannulation ne devrait rien supposer de la politique dinterruption des threads. Un thread ne devrait tre interrompu que par son propritaire ; ce dernier peut encapsuler la connaissance de la politique dinterruption du thread dans un mcanisme dannulation adquat une mthode darrt, par exemple.
Chaque thread ayant sa propre politique dinterruption, vous devriez interrompre un thread que si vous savez ce que cela signie pour lui.

Certaines critiques se sont moques des interruptions de Java car elles ne permettent pas de faire des interruptions premptives et forcent pourtant les dveloppeurs traiter InterruptedException. Cependant, la possibilit de reporter la prise en compte dune demande dinterruption permet aux dveloppeurs de crer des politiques dinterruption souples qui trouvent un quilibre entre ractivit et robustesse en fonction de lapplication. 7.1.3 Rpondre aux interruptions

Comme on la mentionn dans la section 5.4, il y a deux stratgies possibles pour traiter une InterruptedException lorsquon appelle une mthode bloquante interruptible comme Thread.sleep() ou BlockingQueue.put() :
m

propager lexception (ventuellement aprs un peu de mnage spcique la tche), ce qui rend galement votre mthode bloquante et interruptible ; restaurer lindicateur dinterruption pour que le code situ plus haut dans la pile des appels puisse la traiter.

La propagation de InterruptedException consiste simplement ajouter le nom de cette classe la clause throws, comme le fait la mthode getNextTask() du Listing 7.6.

Chapitre 7

Annulation et arrt

147

Listing 7.6 : Propagation de InterruptedException aux appelants.


BlockingQueue <Task> queue; ... public Task getNextTask() throws InterruptedException { return queue.take(); }

Si vous ne voulez pas ou ne pouvez pas propager InterruptedException (parce que votre tche est dnie par un Runnable, par exemple), vous devez utiliser un autre moyen pour prserver la demande dinterruption. Pour ce faire, la mthode standard consiste restaurer lindicateur dinterruption en appelant nouveau interrupt(). Vous ne devez pas absorber InterruptedException en la capturant pour ne rien en faire, sauf si votre code implmente la politique dinterruption dun thread. La classe PrimeProducer absorbe linterruption, mais le fait en sachant que le thread va se terminer et quil ny a donc pas de code plus haut dans la pile dappels qui a besoin de savoir que cette interruption a eu lieu. Mais, dans la plupart des cas, un code ne connat pas le thread quil excutera et il devrait donc prserver lindicateur dinterruption.
Seul le code qui implmente la politique dinterruption dun thread peut absorber une demande dinterruption. Les tches gnrales et le code dune bibliothque ne devraient jamais le faire.

Les activits qui ne reconnaissent pas les annulations mais qui appellent quand mme des mthodes bloquantes interruptibles devront les appeler dans une boucle, en ressayant lorsque linterruption a t dtecte. Dans ce cas, elles doivent sauvegarder localement lindicateur dinterruption et le restaurer juste avant de se terminer, comme le montre le Listing 7.7, plutt quimmdiatement lorsquelles capturent InterruptedException. Positionner trop tt lindicateur dinterruption pourrait provoquer une boucle sans n car la plupart des mthodes bloquantes interruptibles le testent avant de commencer et lancent immdiatement InterruptedException sil est positionn (ces mthodes interrogent cet indicateur avant de se bloquer ou deffectuer un travail un peu important an de ragir le plus vite possible une interruption).
Listing 7.7 : Tche non annulable qui restaure linterruption avant de se terminer.
public Task getNextTask(BlockingQueue <Task> queue) { boolean interrupted = false; try { while (true) { try { return queue.take(); } catch (InterruptedException e) { interrupted = true; // Ne fait rien et ressaie } }

148

Structuration des applications concurrentes

Partie II

Listing 7.7 : Tche non annulable qui restaure linterruption avant de se terminer. (suite)
} finally { if (interrupted) Thread.currentThread().interrupt(); } }

Si votre code nappelle pas de mthodes bloquantes interruptibles, il peut quand mme ragir aux interruptions en interrogeant lindicateur dinterruption du thread courant dans le code de la tche. Choisir la frquence de cette consultation relve dun compromis entre efcacit et ractivit : si vous devez tre trs ractif, vous ne pouvez pas vous permettre dappeler des mthodes susceptibles de durer longtemps et qui ne sont pas elles-mmes ractives aux interruptions ; cela peut donc vous limiter dans vos appels. Lannulation peut impliquer dautres tats que celui dinterruption ; ce dernier peut servir attirer lattention du thread et les informations stockes ailleurs par le thread qui interrompt peuvent tre utilises pour fournir des instructions supplmentaires au thread interrompu (il faut bien sr utiliser une synchronisation lorsque lon accde ces informations). Lorsquun thread dtenu par un ThreadPoolExecutor dtecte une interruption, par exemple, il vrie si le pool est en cours darrt, auquel cas il peut faire un peu de nettoyage avant de se terminer ; sinon il peut crer un autre thread pour que le pool de threads puisse garder la mme taille. 7.1.4 Exemple : excution avec dlai

De nombreux problmes peuvent mettre un temps inni (lnumration de tous les nombres premiers, par exemple) ; pour dautres, la rponse pourrait tre trouve assez vite mais ils peuvent galement durer ternellement. Or, dans certains cas, il peut tre utile de pouvoir dire "consacre dix minutes trouver la rponse" ou "numre toutes les rponses possibles pendant dix minutes". La mthode aSecondOfPrimes() du Listing 7.2 lance un objet PrimeGenerator et linterrompt aprs 1 seconde. Bien que ce dernier puisse mettre un peu plus de 1 seconde pour sarrter, il nira par noter linterruption et cessera son excution, ce qui permettra au thread de se terminer. Cependant, un autre aspect de lexcution dune tche est que lon veut savoir si elle lance une exception : si PrimeGenerator lance une exception non contrle avant lexpiration du dlai, celle-ci passera srement inaperue puisque le gnrateur de nombres premiers sexcute dans un thread spar qui ne gre pas explicitement les exceptions. Le Listing 7.8 est une tentative dexcuter un Runnable quelconque pendant un certain temps. Il lance la tche dans le thread appelant et planie une tche dannulation pour linterrompre aprs un certain dlai. Ceci rgle le problme des exceptions non contrles lances partir de la tche puisquelles peuvent maintenant tre captures par celui qui a appel timedRun().

Chapitre 7

Annulation et arrt

149

Listing 7.8 : Planification dune interruption sur un thread emprunt. Ne le faites pas.
private static final ScheduledExecutorService cancelExec = ...; public static void timedRun(Runnable r, long timeout, TimeUnit unit) { final Thread taskThread = Thread.currentThread(); cancelExec.schedule(new Runnable() { public void run() { taskThread.interrupt(); } }, timeout, unit); r.run(); }

Cette approche est dune simplicit sduisante, mais elle viole la rgle qui nonce que lon devrait connatre la politique dinterruption dun thread avant de linterrompre. timedRun() pouvant tre appele partir de nimporte quel thread, elle ne peut donc connatre la politique dinterruption du thread appelant. Si la tche se termine avant lexpiration du dlai, la tche dannulation qui interrompt le thread dans lequel timedRun() a rendu la main son appelant peut continuer fonctionner en dehors de tout contexte. Nous ne savons pas quel code sexcutera lorsque cela se passera, mais le rsultat ne sera pas correct (il est possible, mais assez compliqu, dliminer ce risque en utilisant lobjet ScheduledFuture renvoy par schedule() pour annuler la tche dannulation). En outre, si la tche ne rpond pas aux interruptions, timedRun() ne se terminera pas tant que la tche ne sest pas termine, ce qui peut tre bien aprs le dlai souhait (voire jamais). Un service dexcution temporis qui ne se termine pas aprs le temps indiqu risque dirriter ses clients. Le Listing 7.9 corrige le problme de gestion des exceptions de aSecondOfPrimes() et les dfauts de la tentative prcdente. Le thread cr pour excuter la tche peut avoir sa propre politique dexcution et la mthode run() temporise peut revenir lappelant, mme si la tche ne rpond pas linterruption. Aprs avoir lanc le thread de la tche, timedRun() excute un join temporis avec le thread nouvellement cr. Lorsque ce join sest termin, elle teste si une exception a t lance partir de la tche, auquel cas elle la relance dans le thread qui a appel timedRun(). Lobjet Throwable sauvegard est partag entre les deux threads et est donc dclar comme volatile pour pouvoir tre correctement publi du thread de la tche vers celui de timedRun().
Listing 7.9 : Interruption dune tche dans un thread ddi.
public static void timedRun(final Runnable r, long timeout, TimeUnit unit) throws InterruptedException { class RethrowableTask implements Runnable { private volatile Throwable t; public void run() { try { r.run(); } catch (Throwable t) { this.t = t; } } void rethrow() { if (t != null) throw launderThrowable(t); } }

150

Structuration des applications concurrentes

Partie II

Listing 7.9 : Interruption dune tche dans un thread ddi. (suite)


RethrowableTask task = new RethrowableTask(); final Thread taskThread = new Thread(task); taskThread.start(); cancelExec.schedule(new Runnable() { public void run() { taskThread.interrupt(); } }, timeout, unit); taskThread.join(unit.toMillis(timeout)); task.rethrow(); }

Cette version corrige les problmes des exemples prcdents mais, comme elle repose sur un join temporis, elle partage une lacune de join : on ne sait pas si lon est revenu de son appel parce que le thread sest termin normalement ou parce que le dlai du join a expir1. 7.1.5 Annulation avec Future

Nous avons dj utilis une abstraction pour grer le cycle de vie dune tche, traiter les exceptions et faciliter lannulation Future. Si lon suit le principe gnral selon lequel il est prfrable dutiliser les classes existantes de la bibliothque plutt que construire les siennes, nous pouvons crire timedRun() en utilisant Future et le framework dexcution des tches.
ExecutorService.submit() renvoie un objet Future dcrivant la tche et Future dispose dune mthode cancel() qui attend un paramtre boolen, mayInterruptIfRunning. Cette mthode renvoie une valeur indiquant si la tentative dannulation a russi (cette valeur indique seulement si lon a t capable de dlivrer linterruption, pas si la tche la dtecte et y a ragi). Si mayInterruptIfRunning vaut true et que la tche soit en cours dexcution dans un thread, celui-ci est interrompu. Sil vaut false, cela signie "nexcute pas cette tche si elle na pas encore t lance" on ne devrait lutiliser que pour les tches qui nont pas t conues pour traiter les interruptions.

Comme on ne devrait pas interrompre un thread sans connatre sa politique dinterruption, quand peut-on appeler cancel() avec un paramtre gal true ? Les threads dexcution des tches crs par les implmentations standard de Executor utilisant une politique dinterruption qui autorise lannulation des tches avec des interruptions, vous pouvez positionner mayInterruptIfRunning true lorsque vous annulez des tches en passant par leurs objets Future lorsquelles sexcutent dans un Executor standard. Lorsque vous voulez annuler une tche, vous ne devriez pas interrompre directement un thread dun pool car vous ne savez pas quelle tche sexcute lorsque la demande dinterruption est dlivre vous devez passer par le Future de la tche. Cest encore une autre raison
1. Il sagit dun dfaut de lAPI Thread car le fait que le join se termine avec succs ou non a des consquences sur la visibilit de la mmoire dans le modle mmoire de Java, or join ne renvoie rien qui puisse indiquer sil a russi ou non.

Chapitre 7

Annulation et arrt

151

pour laquelle il faut coder les tches pour quelles traitent les interruptions comme des demandes dannulation : elles peuvent ainsi tre annules via leur Future. Le Listing 7.10 prsente une version de timedRun() qui soumet la tche un Executor Service et rcupre le rsultat par un appel Future.get() temporis. Si get() se termine avec une exception TimeoutException, la tche est annule via son Future (pour simplier le codage, cette version appelle sans condition Future.cancel() dans un bloc finally an de proter du fait que lannulation dune tche dj termine na aucun effet). Si le calcul sous-jacent lance une exception avant lannulation, celle-ci est relance par timedRun(), ce qui est le moyen le plus pratique pour que lappelant puisse traiter cette exception. Le Listing 7.10 illustre galement une autre pratique : lannulation des tches du rsultat desquelles on na plus besoin (cette technique tait galement utilise dans les Listing 6.13 et 6.16).
Listing 7.10 : Annulation dune tche avec Future.
public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException { Future<?> task = taskExec.submit(r); try { task.get(timeout, unit); } catch (TimeoutException e) { // La tche sera annule en dessous } catch (ExecutionException e) { // Lexception lance dans la tche est relance throw launderThrowable (e.getCause()); } finally { // Sans effet si la tche sest dj termine task.cancel(true); // interruption si la tche sexcute } }

Lorsque Future.get() lance une exception InterruptedException ou Timeout Exception et que lon sait que le rsultat nest plus ncessaire au programme, on annule la tche avec Future.cancel().

7.1.6

Mthodes bloquantes non interruptibles

De nombreuses mthodes bloquantes de la bibliothque rpondent aux interruptions en se terminant prcocment et en lanant une InterruptedException, ce qui facilite la cration de tches ractives aux annulations. Cependant, toutes les mthodes ou tous les mcanismes bloquants ne rpondent pas aux interruptions ; si un thread est bloqu en attente dune E/S synchrone sur une socket ou en attente dun verrou interne, linterruption ne fera que positionner son indicateur dinterruption. Nous pouvons parfois convaincre les threads bloqus dans des activits non interruptibles de se terminer par un moyen ressemblant aux interruptions, mais cela ncessite de savoir plus prcisment pourquoi ils sont bloqus.

152

Structuration des applications concurrentes

Partie II

E/S sockets synchrones de java.io. Cest une forme classique dE/S bloquantes dans les applications serveur lorsquelles lisent ou crivent dans une socket. Malheureusement, les mthodes read() et write() de InputStream et OutputStream ne rpondent pas aux interruptions ; cependant, la fermeture de la socket sous-jacente forcera tout thread bloqu dans read() ou write() lancer une SocketException. E/S synchrones de java.nio. Linterruption dun thread en attente dun InterruptibleChannel le force lancer une exception ClosedByInterruptException et fermer le canal (tous les autres threads bloqus sur ce canal lanceront galement ClosedByInterruptException). La femeture dun InterruptibleChannel force les threads bloqus dans des oprations sur ce canal lancer AsynchronousClose Exception. La plupart des Channel standard implmentent InterruptibleChannel. E/S asynchrones avec Selector. Si un thread est bloqu dans Selector.select() (dans java.nio.channels), un appel close() le force se terminer prmaturment. Acquisition dun verrou. Si un thread est bloqu en attente dun verrou interne, vous ne pouvez pas lempcher de sassurer quil nira par prendre le verrou et de progresser sufsamment pour attirer son attention dune autre faon. Cependant, les classes Lock explicites disposent de la mthode lockInterruptibly(), qui vous permet dattendre un verrou tout en pouvant rpondre aux interruptions voir le Chapitre 13.

La classe ReaderThread du Listing 7.11 prsente une technique pour encapsuler les annulations non standard. Elle gre une connexion par socket simple, lit dans la socket de faon synchrone et passe processBuffer() les donnes quelle a reues. Pour faciliter la terminaison dune connexion utilisateur ou larrt du serveur, ReaderThread rednit interrupt() pour quelle dlivre une interruption standard et ferme la socket sous-jacente. Linterruption dun ReaderThread arrte donc ce quil tait en train de faire, quil soit bloqu dans read() ou dans une mthode bloquante interruptible.
Listing 7.11 : Encapsulation des annulations non standard dans un thread par redfinition de interrupt().
public class ReaderThread extends Thread { private final Socket socket; private final InputStream in; public ReaderThread(Socket socket) throws IOException { this.socket = socket; this.in = socket.getInputStream(); } public void interrupt() { try { socket.close(); } catch (IOException ignored) { } finally {

Chapitre 7

Annulation et arrt

153

super.interrupt(); } } public void run() { try { byte[] buf = new byte[BUFSZ]; while (true) { int count = in.read(buf); if (count < 0) break; else if (count > 0) processBuffer(buf, count); } } catch (IOException e) { /* Permet au thread de se terminer */ } } }

7.1.7

Encapsulation dune annulation non standard avec newTaskFor()

La technique utilise par ReaderThread pour encapsuler une annulation non standard peut tre amliore en utilisant la mthode de rappel newTaskFor(), ajoute Thread PoolExecutor depuis Java 6. Lorsquun Callable est soumis un ExecutorService, submit() renvoie un Future pouvant servir annuler la tche. newTaskFor() est une mthode fabrique qui cre le Future reprsentant la tche ; elle renvoie un Runnable Future, une interface tendant la fois Future et Runnable (et implmente par Future Task). Vous pouvez rednir Future.cancel() pour personnaliser la tche Future. Personnaliser le code dannulation permet dinscrire dans un chier journal des informations sur lannulation, par exemple, et vous pouvez galement en proter pour annuler des activits qui ne rpondent pas aux interruptions. Tout comme ReaderThread encapsule lannulation des threads qui utilisent des sockets en rednissant interrupt(), vous pouvez faire de mme pour les tches en rednissant Future.cancel(). Le Listing 7.12 dnit une interface CancellableTask qui tend Callable en lui ajoutant une mthode cancel() et une mthode fabrique newTask() pour crer un objet Runnable Future. CancellingExecutor tend ThreadPoolExecutor et rednit newTaskFor() pour quune CancellableTask puisse crer son propre Future.
SocketUsingTask implmente CancellableTask et dnit Future.cancel() pour quelle ferme la socket et appelle super.cancel(). Si une SocketUsingTask est annule via son Future, la socket est ferme et le thread qui sexcute est interrompu. Ceci augmente la ractivit de la tche lannulation : non seulement elle peut appeler en toute scurit des mthodes bloquantes interruptibles tout en restant rceptive lannulation, mais elle peut galement appeler des mthodes dE/S bloquantes sur des sockets.

154

Structuration des applications concurrentes

Partie II

Listing 7.12 : Encapsulation des annulations non standard avec newTaskFor().


public interface CancellableTask <T> extends Callable<T> { void cancel(); RunnableFuture<T> newTask(); } @ThreadSafe public class CancellingExecutor extends ThreadPoolExecutor { ... protected<T> RunnableFuture <T> newTaskFor(Callable<T> callable) { if (callable instanceof CancellableTask ) return ((CancellableTask <T>) callable).newTask(); else return super.newTaskFor(callable); } } public abstract class SocketUsingTask <T> implements CancellableTask <T> { @GuardedBy("this") private Socket socket; protected synchronized void setSocket(Socket s) { socket = s; } public synchronized void cancel() { try { if (socket != null) socket.close(); } catch (IOException ignored) { } } public RunnableFuture <T> newTask() { return new FutureTask<T>(this) { public boolean cancel(boolean mayInterruptIfRunning ) { try { SocketUsingTask .this.cancel(); } finally { return super.cancel(mayInterruptIfRunning ); } } }; } }

7.2

Arrt dun service reposant sur des threads

Les applications crent souvent des services qui utilisent des threads comme des pools de threads et la dure de vie de ces services est gnralement plus longue que celle de la mthode qui les a crs. Si lapplication doit sarrter en douceur, il faut mettre n aux threads appartenant ces services. Comme il ny a pas moyen dimposer un thread de sarrter, il faut les persuader de se terminer deux-mmes. Les bonnes pratiques dencapsulation enseignent quil ne faut pas manipuler un thread linterrompre, modier sa priorit, etc. qui ne nous appartient pas. LAPI des threads ne dnit pas formellement la proprit dun thread : un thread est reprsent par un objet Thread qui peut tre librement partag, exactement comme nimporte quel autre objet. Cependant, il semble raisonnable de penser quun thread a un propritaire,

Chapitre 7

Annulation et arrt

155

qui est gnralement la classe qui la cr. Un pool de threads est donc le propritaire de ses threads et, sils doivent tre interrompus, cest le pool qui devrait sen occuper. Comme pour tout objet encapsul, la proprit dun thread nest pas transitive : lapplication peut possder le service et celui-ci peut possder les threads, mais lapplication ne possde pas les threads et ne peut donc pas les arrter directement. Le service doit donc fournir des mthodes de cycle de vie pour se terminer lui-mme et arrter galement les threads quil possde ; lapplication peut alors arrter le service, qui se chargera luimme darrter les threads. La classe ExecutorService fournit les mthodes shutdown() et shutdownNow() ; les autres services possdant des threads devraient proposer un mcanisme darrt similaire.
Fournissez des mthodes de cycle de vie chaque fois quun service possdant des threads a une dure de vie suprieure celle de la mthode qui la cr.

7.2.1

Exemple : service de journalisation

La plupart des applications serveur crivent dans un journal, ce qui peut tre aussi simple quinsrer des instructions println() dans le code. Les classes de ux comme Print Writer tant thread-safe, cette approche simple ne ncessiterait aucune synchronisation explicite1. Cependant, comme nous le verrons dans la section 11.6, une journalisation en ligne peut avoir un certain cot en termes de performances pour les applications fort volume. Une autre possibilit consiste mettre les messages du journal dans une le dattente pour quils soient traits par un autre thread. La classe LogWriter du Listing 7.13 montre un service simple de journalisation dans lequel lactivit dinscription dans le journal a t dplace dans un thread spar. Au lieu que le thread qui produit le message lcrive directement dans le ux de sortie, LogWriter le passe un thread dcriture via une BlockingQueue. Il sagit donc dune conception de type "plusieurs producteurs, un seul consommateur" o les activits productrices crent les messages et lactivit consommatrice les crit. Si le thread consommateur va moins vite que les producteurs, la BlockingQueue bloquera ces derniers jusqu ce que le thread dcriture dans le journal rattrape son retard.

1. Si le mme message se dcompose en plusieurs lignes, vous pouvez galement avoir besoin dun verrouillage ct client pour empcher un entrelacement indsirable des messages des diffrents threads. Si deux threads inscrivent, par exemple, des traces dexcution sur plusieurs lignes dans le mme ux avec une instruction println() par ligne, le rsultat serait entrelac alatoirement et pourrait donner une seule grande trace inexploitable.

156

Structuration des applications concurrentes

Partie II

Listing 7.13 : Service de journalisation producteur-consommateur sans support de larrt.


public class LogWriter { private final BlockingQueue <String> queue; private final LoggerThread logger; public LogWriter(Writer writer) { this.queue = new LinkedBlockingQueue <String>(CAPACITY); this.logger = new LoggerThread(writer); } public void start() { logger.start(); } public void log(String msg) throws InterruptedException { queue.put(msg); } private class LoggerThread extends Thread { private final PrintWriter writer; ... public void run() { try { while (true) writer.println(queue.take()); } catch(InterruptedException ignored) { } finally { writer.close(); } } } }

Pour quun service comme LogWriter soit utile en production, nous avons besoin de pouvoir terminer le thread dcriture dans le journal sans quil empche larrt normal de la JVM. Arrter ce thread est assez simple puisquil appelle take() en permanence et que cette mthode rpond aux interruptions ; si lon modie ce thread pour quil sarrte lorsquil capture InterruptedException, son interruption arrtera le service. Cependant, faire simplement en sorte que le thread dcriture se termine nest pas un mcanisme darrt trs satisfaisant. En effet, cet arrt brutal supprime les messages qui pourraient tre en attente dcriture et, ce qui est encore plus important, les threads producteurs bloqus parce que la le est pleine ne seront jamais dbloqus. Lannulation dune activit producteur-consommateur exige dannuler la fois les producteurs et les consommateurs. Interrrompre le thread dcriture rgle le problme du consommateur mais, les producteurs ntant pas ici des threads ddis, il est plus difcile de les annuler. Une autre approche pour arrter LogWriter consiste positionner un indicateur "arrt demand" pour empcher que dautres messages soient soumis, comme on le montre dans le Listing 7.14. Lorsquil est prvenu de cette demande, le consommateur peut alors vider la le en inscrivant dans le journal les messages en attente et en dbloquant les ventuels producteurs bloqus dans log(). Cependant, cette approche a des situations de comptition qui la rendent non able. Limplmentation du journal est une squence tester-puis-agir : les producteurs pourraient constater que le service na pas encore t arrt et continuer placer des messages dans la le aprs larrt avec le risque de se

Chapitre 7

Annulation et arrt

157

trouver nouveau bloqus indniment dans log(). Il existe des astuces rduisant cette probabilit (en faisant, par exemple, attendre quelques secondes le consommateur avant de dclarer la le comme puise), mais elles changent non pas le problme fondamental, mais uniquement la probabilit que ce problme survienne.
Listing 7.14 : Moyen non fiable dajouter larrt au service de journalisation.
public void log(String msg) throws InterruptedException { if (!shutdownRequested ) queue.put(msg); else throw new IllegalStateException("logger is shut down"); }

Pour fournir un arrt able LogWriter, il faut rgler le problme de la situation de comptition, ce qui signie quil faut que la soumission dun nouveau message soit atomique. Cependant, nous ne voulons pas dtenir un verrou pendant que lon place le message dans la le car put() pourrait se bloquer. Nous pouvons, en revanche, vrier de faon atomique quil y a eu demande darrt et incrmenter un compteur, an de nous "rserver" le droit de soumettre un message, comme dans le Listing 7.15.
Listing 7.15 : Ajout dune annulation fiable LogWriter.
public class LogService { private final BlockingQueue <String> queue; private final LoggerThread loggerThread; private final PrintWriter writer; @GuardedBy("this") private boolean isShutdown; @GuardedBy("this") private int reservations; public void start() { loggerThread.start(); } public void stop() { synchronized (this) { isShutdown = true; } loggerThread.interrupt(); } public void log(String msg) throws InterruptedException { synchronized (this) { if (isShutdown) throw new IllegalStateException (...); ++reservations; } queue.put(msg); } private class LoggerThread extends Thread { public void run() { try { while (true) { try { synchronized (LogService.this) { if (isShutdown && reservations == 0) break; } String msg = queue.take(); synchronized (LogService.this) { --reservations ; }

158

Structuration des applications concurrentes

Partie II

Listing 7.15 : Ajout dune annulation fiable LogWriter. (suite)


writer.println(msg); } catch (InterruptedException e) { /* ressaie */ } } } finally { writer.close(); } } } }

7.2.2

Mthodes darrt de ExecutorService

Dans la section 6.2.4, nous avons vu que ExecutorService offrait deux moyens de sarrter : un arrt en douceur avec shutdown() et un arrt brutal avec shutdownNow(). Dans ce dernier cas, shutdownNow() renvoie la liste des tches qui nont pas encore commenc aprs avoir tent dannuler toutes les tches qui sexcutent. Ces deux options de terminaison sont des compromis entre scurit et ractivit : la terminaison brutale est plus rapide mais plus risque car les tches peuvent tre interrompues au milieu de leur excution, tandis que la terminaison normale est plus lente mais plus sre puisque ExecutorService ne sarrtera pas tant que toutes les tches en attente nauront pas t traites. Les autres services utilisant des threads devraient fournir un choix quivalent pour leurs modes darrt. Les programmes simples peuvent sen sortir en lanant et en arrtant un ExecutorService global partir de main(). Ceux qui sont plus sophistiqus encapsuleront srement un ExecutorService derrire un service de plus haut niveau fournissant ses propres mthodes de cycle de vie, comme le fait la variante de LogService dans le Listing 7.16, qui dlgue la gestion de ses propres threads un ExecutorService. Cette encapsulation tend la chane de proprit de lapplication au service et au thread en ajoutant un autre lien ; chaque membre de cette chane gre le cycle de vie des services ou des threads qui lui appartiennent.
Listing 7.16 : Service de journalisation utilisant un ExecutorService.
public class LogService { private final ExecutorService exec = newSingleThreadExecutor (); ... public void start() { } public void stop() throws InterruptedException { try { exec.shutdown(); exec.awaitTermination(TIMEOUT, UNIT); } finally { writer.close(); } } public void log(String msg) { try { exec.execute(new WriteTask(msg)); } catch (RejectedExecutionException ignored) { } } }

Chapitre 7

Annulation et arrt

159

7.2.3

Pilules empoisonnes

Un autre moyen de convaincre un service producteur-consommateur de sarrter consiste utiliser une pilule empoisonne, cest--dire un objet reconnaissable plac dans la le dattente, qui signie "quand tu me prends, arrte-toi". Avec une le FIFO, les pilules empoisonnes garantissent que les consommateurs niront leur travail sur leur le avant de sarrter, puisque tous les travaux soumis avant la pilule seront rcuprs avant elle ; les producteurs ne devraient pas soumettre de nouveaux travaux aprs avoir mis la pilule dans la le. Dans les Listings 7.17, 7.18 et 7.19, la classe IndexingService montre une version "un producteur-un consommateur" de lexemple dindexation du disque que nous avions prsent dans le Listing 5.8. Elle utilise une pilule empoisonne pour arrter le service.
Listing 7.17 : Arrt dun service avec une pilule empoisonne.
public class IndexingService { private static final File POISON = new File(""); private final IndexerThread consumer = new IndexerThread(); private final CrawlerThread producer = new CrawlerThread(); private final BlockingQueue <File> queue; private final FileFilter fileFilter; private final File root; class CrawlerThread extends Thread { /* Listing 7.18 */ } class IndexerThread extends Thread { /* Listing 7.19 */ } public void start() { producer.start(); consumer.start(); } public void stop() { producer.interrupt(); } public void awaitTermination() throws InterruptedException { consumer.join(); } }

Listing 7.18 : Thread producteur pour IndexingService.


public class CrawlerThread extends Thread { public void run() { try { crawl(root); } catch (InterruptedException e) { /* ne fait rien */ } finally { while (true) { try { queue.put(POISON); break; } catch (InterruptedException e1) { /* ressaie */ } } } } private void crawl(File root) throws InterruptedException { ... } }

160

Structuration des applications concurrentes

Partie II

Listing 7.19 : Thread consommateur pour IndexingService.


public class IndexerThread extends Thread { public void run() { try { while (true) { File file = queue.take(); if (file == POISON) break; else indexFile(file); } } catch (InterruptedException consumed) { } } }

Les pilules empoisonnes ne conviennent que lorsque lon connat le nombre de producteurs et de consommateurs. Lapproche utilise par IndexingService peut tre tendue plusieurs producteurs : chacun deux place une pilule dans la le et le consommateur ne sarrte que lorsquil a reu Nproducteurs pilules. Elle peut galement tre tendue plusieurs consommateurs : chaque producteur place alors N consommateurs pilules dans la le, bien que cela puisse devenir assez lourd avec un grand nombre de producteurs et de consommateurs. Les pilules empoisonnes ne fonctionnent correctement quavec des les non bornes. 7.2.4 Exemple : un service dexcution phmre

Si une mthode doit traiter un certain nombre de tches et quelle ne se termine que lorsque toutes ces tches sont termines, la gestion du cycle de vie du service peut tre simplie en utilisant un Executor priv dont la dure de vie est limite par cette mthode (dans ces situations, les mthodes invokeAll() et invokeAny() sont souvent utiles). La mthode checkMail() du Listing 7.20 teste en parallle si du nouveau courrier est arriv sur un certain nombre dhtes. Elle cre un excuteur priv et soumet une tche pour chaque hte : puis elle arrte lexcuteur et attend la n, qui intervient lorsque toutes les tches de vrication du courrier se sont termines1.
Listing 7.20 : Utilisation dun Executor priv dont la dure de vie est limite un appel de mthode.
boolean checkMail(Set<String> hosts, long timeout, TimeUnit unit) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool (); final AtomicBoolean hasNewMail = new AtomicBoolean(false); try { for (final String host : hosts) exec.execute(new Runnable() { public void run() { if (checkMail(host)) hasNewMail.set(true);

1. On utilise un AtomicBoolean au lieu dun boolen volatile car, pour accder lindicateur hasNewMail partir du Runnable interne, celui-ci devrait tre final, ce qui empcherait de le modier.

Chapitre 7

Annulation et arrt

161

} }); } finally { exec.shutdown(); exec.awaitTermination(timeout, unit); } return hasNewMail.get(); }

7.2.5

Limitations de shutdownNow()

Lorsquun ExecutorService est arrt brutalement par un appel shutdownNow(), il tente dannuler les tches en cours et renvoie une liste des tches qui ont t soumises mais jamais lances an de pouvoir les inscrire dans un journal ou les sauvegarder pour un traitement ultrieur1. Cependant, il nexiste pas de mthode gnrale pour savoir quelles sont les tches qui ont t lances et qui ne sont pas encore termines. Ceci signie que vous ne pouvez pas connatre ltat des tches en cours au moment de larrt, sauf si ces tches effectuent elles-mmes une sorte de pointage. Pour connatre les tches qui ne sont pas encore termines, vous devez savoir non seulement quelles tches nont pas dmarr mais galement celles qui taient en cours lorsque lexcuteur a t arrt 2. La classe TrackingExecutor du Listing 7.21 montre une technique permettant de dterminer les tches en cours au moment de larrt. En encapsulant un ExecutorService et en modiant execute() (et submit(), mais ce nest pas montr ici) pour quelle se rappelle les tches qui ont t annules aprs larrt, TrackingExecutor peut savoir quelles tches ont t lances mais ne se sont pas termines normalement. Lorsque lexcuteur sest termin, getCancelledTasks() renvoie la liste des tches annules. Pour que cette technique fonctionne, les tches doivent prserver lindicateur dinterruption du thread lorsquelles se terminent, ce que font toutes les tches correctes de toute faon.
Listing 7.21 : ExecutorService mmorisant les tches annules aprs larrt.
public class TrackingExecutor extends AbstractExecutorService { private final ExecutorService exec; private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet (new HashSet<Runnable>()); ... public List<Runnable> getCancelledTasks () { if (!exec.isTerminated()) throw new IllegalStateException (...); return new ArrayList<Runnable>(tasksCancelledAtShutdown); }

1. Les objets Runnable renvoys par shutdownNow() peuvent ne pas tre les mmes que ceux qui ont t soumis lExecutorService : il peut sagir dinstances enveloppes de ces tches. 2. Malheureusement, il nexiste pas doption darrt permettant de renvoyer lappelant les tches qui nont pas encore dmarr et de laisser terminer celles qui sont en cours. Cette option permettrait dliminer cet tat intermdiaire incertain.

162

Structuration des applications concurrentes

Partie II

public void execute(final Runnable runnable) { exec.execute(new Runnable() { public void run() { try { runnable.run(); } finally { if (isShutdown() && Thread.currentThread().isInterrupted()) tasksCancelledAtShutdown.add(runnable); } } }); } // Dlgue exec les autres mthodes de ExecutorService }

La classe WebCrawler du Listing 7.22 met en uvre TrackingExecutor. Le travail dun robot web est souvent une tche sans n : sil doit tre arrt, il est prfrable de sauvegarder son tat courant an de pouvoir le relancer plus tard. CrawlTask fournit une mthode getPage() qui identie la page courante. Lorsque le robot est arrt, les tches qui nont pas t lances et celles qui ont t annules sont analyses et leurs URL sont enregistres an que les tches de parcours de ces URL puissent tre ajoutes la le lorsque le robot repart.
Listing 7.22 : Utilisation de TrackingExecutorService pour mmoriser les tches non termines afin de les relancer plus tard.
public abstract class WebCrawler { private volatile TrackingExecutor exec; @GuardedBy("this") private final Set<URL> urlsToCrawl = new HashSet<URL>(); ... public synchronized void start() { exec = new TrackingExecutor ( Executors.newCachedThreadPool()); for (URL url : urlsToCrawl) submitCrawlTask (url); urlsToCrawl.clear(); } public synchronized void stop() throws InterruptedException { try { saveUncrawled(exec.shutdownNow()); if (exec.awaitTermination(TIMEOUT, UNIT)) saveUncrawled(exec.getCancelledTasks()); } finally { exec = null; } } protected abstract List<URL> processPage(URL url); private void saveUncrawled(List<Runnable> uncrawled) { for (Runnable task : uncrawled) urlsToCrawl.add(((CrawlTask) task).getPage()); } private void submitCrawlTask (URL u) { exec.execute(new CrawlTask(u)); } private class CrawlTask implements Runnable { private final URL url; ...

Chapitre 7

Annulation et arrt

163

public void run() { for (URL link : processPage(url)) { if (Thread.currentThread().isInterrupted()) return; submitCrawlTask (link); } } public URL getPage() { return url; } } }

TrackingExecutor a une situation de comptition invitable qui pourrait produire des faux positifs, cest--dire des tches identies comme annules alors quelles sont en fait termines. Cette situation se produit lorsque le pool de threads est arrt entre le moment o la dernire instruction de la tche sexcute et celui o le pool considre que la tche est termine. Ceci nest pas un problme si les tches sont idempotentes (les excuter deux fois aura le mme effet que ne les excuter quune seule fois), ce qui est gnralement le cas dans un robot web. Dans le cas contraire, lapplication qui rcupre les tches annules doit tre au courant de ce risque et prte grer les faux positifs.

7.3

Gestion de la n anormale dun thread

On saperoit immdiatement quune application monothread en mode console se termine cause dune exception non capture le programme sarrte et afche une trace de la pile dappels qui est trs diffrente de la sortie dun programme normal. Lchec dun thread dans une application concurrente, en revanche, nest pas toujours aussi vident. La trace de la pile peut safcher sur la console mais on peut trs bien ne pas regarder la console. En outre, lorsquun thread choue, lapplication peut sembler continuer fonctionner et cet chec peut ne pas tre remarqu. Heureusement, il existe des moyens de dtecter et dempcher les "fuites" de threads dune application. La cause principale de la mort prmature dun thread est RuntimeException. Ces exceptions indiquant une erreur de programmation ou un autre problme irrcuprable, elles ne sont gnralement pas captures et se propagent donc jusquau sommet de la pile des appels, o le comportement par dfaut consiste afcher une trace de cette pile sur la console et mettre n au thread. Selon le rle du thread dans lapplication, les consquences de sa mort anormale peuvent tre bnignes ou dsastreuses. Perdre un thread dun pool de threads peut avoir des consquences sur les performances, mais une application qui sexcute correctement avec un pool de 50 threads fonctionnera srement tout aussi bien avec un pool de 49 threads. En revanche, perdre le thread de rpartition des vnements dans une application graphique ne passera pas inaperu lapplication ne pourra plus traiter les vnements et linterface se gera. La classe OutOfTime au Listing 6.9 montrait une consquence grave de la fuite dun thread : le service reprsent par le Timer tait dnitivement hors dtat.

164

Structuration des applications concurrentes

Partie II

Quasiment nimporte quel code peut lancer une exception RuntimeException. chaque fois que lon appelle une autre mthode, on suppose quelle se terminera normalement ou lancera lune des exceptions contrles dclares dans sa signature. Moins lon connat le code qui est appel, moins on doit avoir conance en son comportement. Les threads de traitement des tches, comme ceux dun pool de threads ou le thread de rpartition des vnements Swing, passent leur existence appeler du code inconnu via une barrire dabstraction comme Runnable : ils devraient donc tre trs mants sur le comportement du code quils appellent. Il serait dsastreux quun service comme le thread des vnements Swing choue uniquement parce quun gestionnaire dvnement mal crit a lanc une exception NullPointerException. En consquence, ces services devraient appeler les tches dans un bloc trycatch pour garantir que le framework sera prvenu si le thread se termine anormalement et quil pourra prendre les mesures ncessaires. Cest lune des rares fois o lon peut envisager de capturer Runtime Exception lorsque lon appelle un code non able au moyen dune abstraction comme Runnable1. Le Listing 7.23 montre une faon de structurer un thread dans un pool de threads. Si une tche lance une exception non contrle, il autorise le thread mourir mais pas avant davoir prvenu le framework de sa mort. Le framework peut alors remplacer ce thread par un nouveau ou ne pas le faire parce que le pool sarrte ou parce quil y a dj sufsamment de threads pour combler la demande courante. ThreadPoolExecutor et Swing utilisent cette technique pour sassurer quune tche qui se comporte mal nempchera pas lexcution des tches suivantes. Lorsque vous crivez une classe thread qui excute les tches qui lui sont soumises ou que vous appelez un code externe non vri (comme des greffons chargs dynamiquement), utilisez lune de ces approches pour empcher les tches ou les greffons mal crits de tuer le thread qui les a appels.
Listing 7.23 : Structure typique dun thread dun pool de threads.
public void run() { Throwable thrown = null; try { while (!isInterrupted()) runTask(getTaskFromWorkQueue()); } catch (Throwable e) { thrown = e; } finally { threadExited(this, thrown); } }

1. La scurit de cette technique fait dbat : lorsquun thread lance une exception non contrle, toute lapplication peut tre compromise. Mais lautre alternative arrter toute lapplication nest gnralement pas envisageable.

Chapitre 7

Annulation et arrt

165

7.3.1

Gestionnaires dexceptions non captures

La section prcdente a propos une approche proactive du problme des exceptions non contrles. LAPI des threads fournit galement le service UncaughtExceptionHandler, qui permet de dtecter la mort dun thread cause dune exception non capture. Les deux approches sont complmentaires : prises ensemble, elles fournissent une dfense en profondeur contre la fuite de threads. Lorsquun thread se termine cause dune exception non capture, la JVM signale cet vnement un objet UncaughtExceptionHandler fourni par lapplication (voir Listing 7.24) ; si aucun gestionnaire nexiste, le comportement par dfaut consiste afcher la trace de la pile sur System.err1.
Listing 7.24 : Interface UncaughtExceptionHandler.
public interface UncaughtExceptionHandler { void uncaughtException(Thread t, Throwable e); }

Ce que doit faire le gestionnaire dune exception non capture dpend des exigences de la qualit du service. La rponse la plus frquente consiste crire un message derreur et une trace de la pile dans le journal de lapplication, comme le montre le Listing 7.25. Les gestionnaires peuvent galement agir plus directement, en essayant de relancer le thread, par exemple, ou en teignant lapplication, en appelant un oprateur ou toute autre action corrective ou de diagnostic.
Listing 7.25 : UncaughtExceptionHandler, qui inscrit lexception dans le journal.
public class UEHLogger implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { Logger logger = Logger.getAnonymousLogger (); logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e); } }

1. Avant Java 5.0, la seule faon de contrler le UncaughtExceptionHandler consistait hriter de ThreadGroup. partir de Java 5.0, on peut obtenir un UncaughtExceptionHandler thread par thread avec Thread.setUncaughtExceptionHandler() et lon peut galement congurer le UncaughtException Handler par dfaut avec Thread.setDefaultUncaughtExceptionHandler(). Cependant, un seul de ces gestionnaires est appel la JVM recherche dabord un gestionnaire par thread, puis un gestionnaire pour un ThreadGroup. Limplmentation du gestionnaire par dfaut dans ThreadGroup dlgue le travail son groupe de thread parent et ainsi de suite jusqu ce que lun des gestionnaires de Thread Group traite lexception non capture ou la fasse remonter au groupe situ le plus haut. Le gestionnaire du groupe de premier niveau dlgue son tour le traitement au gestionnaire par dfaut du systme (sil en existe un car il ny en pas par dfaut) et, sinon, afche la trace de la pile sur la console.

166

Structuration des applications concurrentes

Partie II

Dans les applications qui sexcutent en permanence, utilisez toujours des gestionnaires dexceptions non captures pour que les threads puissent au moins inscrire lexception dans le journal.

Pour congurer un UncaughtExceptionHandler pour les threads de pool, on fournit un ThreadFactory au constructeur de ThreadPoolExecutor (comme pour toute manipulation de threads, seul le propritaire du thread peut changer son UncaughtExceptionHandler). Les pools de threads standard permettent une exception non capture dans une tche de mettre n un thread du pool. Cependant, utilisez un bloc tryfinally pour tre prvenu de cet vnement an de pouvoir remplacer le thread car, sans gestionnaire dexception non capture ou autre mcanisme de notication derreur, les tches peuvent sembler chouer sans rien dire, ce qui peut tre trs troublant. Si vous voulez tre prvenu quune tche choue cause dune exception, enveloppez la tche avec un Runnable ou un Callable qui capture lexception ou rednissez la mthode afterExecute() dans ThreadPoolExecutor. Bien que ce soit un peu confus, les exceptions leves depuis le gestionnaire dexceptions non captures dune tche ne concernent que les tches lances par la mthode execute() ; pour les tches soumises avec submit(), toute exception lance, quelle soit contrle ou non, est considre comme faisant partie de ltat renvoy par la tche. Si une tche soumise par submit() se termine par une exception, celle-ci est relance par Future.get(), enveloppe dans une exception ExecutionException.

7.4

Arrt de la JVM

La machine virtuelle Java (JVM) peut sarrter en douceur ou brutalement. Un arrt en douceur a lieu lorsque le dernier thread "normal" (non dmon) se termine, lorsque quelquun a appel System.exit() ou par un autre moyen spcique la plate-forme (lenvoi du signal SIGINT ou la frappe de Ctrl+C, par exemple). Bien quil sagisse du moyen standard et conseill darrter la JVM, celle-ci peut galement tre arrte brutalement par un appel Runtime.halt() ou en tuant son processus directement partir du systme dexploitation (en lui envoyant le signal SIGKILL, par exemple). 7.4.1 Mthodes dinterception dun ordre darrt

Dans un arrt en douceur, la JVM lance dabord les hooks1 darrt enregistrs, qui sont des threads non lancs et enregistrs avec Runtime.addShutdownHook(). La JVM ne garantit pas lordre dans lequel ces hooks seront lancs. Si des threads de lapplication (dmons ou non) sont en cours dexcution au moment de larrt, ils continueront
1. N.d.T. : Nous laissons le terme hook dans le texte car cest un terme assez courant. Une traduction possible est "mthode dinterception".

Chapitre 7

Annulation et arrt

167

fonctionner en parallle avec le processus darrt. Lorsque tous les hooks darrt se sont termins, la JVM peut choisir de lancer les naliseurs si runFinalizersOnExit vaut true, puis sarrte. La JVM ne tente pas darrter ou dinterrompre les threads de lapplication qui sont en cours dexcution au moment de larrt ; ils seront brutalement termins lorsque la JVM nira par sarrter. Si les hooks ou les naliseurs ne se terminent pas, le processus darrt en douceur "se ge" et la JVM doit alors tre arrte brutalement. Dans un arrt brutal, la JVM nest pas cense faire autre chose que sarrter ; les hooks darrt ne sexcuteront pas. Ceux-ci doivent tre thread-safe : ils doivent utiliser la synchronisation lorsquils acccdent des donnes partages et faire attention ne pas crer de deadlocks, exactement comme nimporte quel autre code concurrent. En outre, ils ne doivent faire aucune supposition sur ltat de lapplication (comme supposer que les autres services se sont dj arrts ou que tous les threads normaux se sont termins) ni sur la raison pour laquelle la JVM sarrte : ils doivent donc tre cods pour tre trs prudents. Enn, ils doivent se terminer le plus vite possible car leur existence retarde larrt de la JVM un moment o lutilisateur sattend ce quelle sarrte rapidement. Les hooks darrt peuvent tre utiliss pour nettoyer le service ou lapplication pour supprimer les chiers temporaires ou fermer des ressources qui ne sont pas automatiquement fermes par le systme dexploitation, par exemple. Le Listing 7.26 montre comment la classe LogService du Listing 7.16 pourrait enregistrer un hook darrt partir de sa mthode start() pour sassurer que le chier journal est ferm la n de lapplication.
Listing 7.26 : Enregistrement dun hook darrt pour arrter le service de journalisation.
public void start() { Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { try { LogService.this.stop(); } catch (InterruptedException ignored) {} } }); }

Les hooks darrt sexcutant tous en parallle, la fermeture du chier journal pourrait poser des problmes aux autres hooks qui veulent lutiliser. Pour viter cette situation, les hooks ne devraient pas utiliser des services qui peuvent tre arrts par lapplication ou les autres hooks. Une faon de procder consiste utiliser un seul hook pour tous les services au lieu dun hook par service et lui faire appeler une srie dactions darrt. Cela garantit que ces actions sexcuteront dans le mme thread et vitera les situations de comptition ou les deadlocks entre elles. Cette technique peut dailleurs tre utilise avec ou sans hooks darrt car une excution dactions en squence plutt quen parallle limine de nombreuses sources potentielles derreur. Dans les applications qui grent des informations de dpendances entre les services, cette technique permet galement de sassurer que les actions darrt sexcuteront dans le bon ordre.

168

Structuration des applications concurrentes

Partie II

7.4.2

Threads dmons

Parfois, on souhaite crer un thread qui effectue une fonction utilitaire mais sans que lexistence de ce thread empche la JVM de sarrter. Cest la raison dtre des threads dmons. Les threads se classent en deux catgories : les threads normaux et les threads dmons. Lorsque la JVM dmarre, tous les threads quelle cre (comme le ramasse-miettes et les autres threads de nettoyage) sont des threads dmons, sauf le thread principal. Tout nouveau thread cr hrite de ltat dmon de celui qui la cr : par dfaut, tous les threads crs par le thread principal sont donc galement des threads normaux. Ces deux catgories ne diffrent que par ce qui se passe lorsquils se terminent. la n dun thread, la JVM effectue un inventaire des threads en cours dexcution et lance un arrt en douceur si les seuls qui restent sont des threads dmons. Lorsque la JVM sarrte, tous les threads dmons restants sont abandonns les blocs finally ne sont pas excuts, les piles ne sont pas libres et la JVM se contente de sarrter. Les threads dmons doivent tre utiliss avec modration car peu dactivits peuvent tre abandonnes brutalement sans aucun nettoyage. Il est notamment dangereux dutiliser des threads dmons susceptibles deffectuer des oprations dE/S. Les threads dmons doivent tre rservs aux tches "domestiques", pour supprimer priodiquement des entres dun cache mmoire, par exemple.
Les threads dmons ne doivent pas se substituer une bonne gestion du cycle de vie des services dans une application.

7.4.3

Finaliseurs

Le ramasse-miettes permet de rcuprer les ressources mmoire lorsquelles ne sont plus ncessaires, mais certaines, comme les descripteurs de chiers ou de sockets, doivent tre restitues explicitement au systme dexploitation lorsquelles ne sont plus utilises. Pour cette raison, le ramasse-miettes traite de faon spciale les objets qui disposent dune mthode finalize() non triviale : aprs avoir t rcuprs par le ramasse-miettes, celui-ci appelle leur mthode finalize() pour que les ressources persistantes puissent tre libres. Les naliseurs pouvant sexcuter dans un thread gr par la JVM, tout tat auquel un naliseur accde sera accd par plusieurs threads et doit donc tre synchronis. Les naliseurs ne garantissent pas linstant o ils seront excuts ni mme sils le seront, et ils ont un impact non ngligeable en terme de performances lorsque leur code est un peu important. Il est galement trs difcile de les crire correctement 1. Dans la plupart des cas, la combinaison de blocs finally et dappels explicites des mthodes close()
1. Voir (Boehm, 2005) pour certains des ds quil faut relever lorsque lon crit des naliseurs.

Chapitre 7

Annulation et arrt

169

est plus efcace que les naliseurs pour la gestion des ressources ; la seule exception concerne la gestion dobjets qui dtiennent des ressources acquises par des mthodes natives. Pour toutes ces raisons, essayez de toujours faire en sorte dviter dcrire ou dutiliser des classes ayant des naliseurs ( part celles de la bibliothque standard) [EJ Item 6].
vitez les naliseurs.

Rsum
Les problmes de n de vie des tches, des threads, des services et des applications peuvent compliquer leur conception et leur implmentation. Java ne fournit pas de mcanisme premptif pour annuler les activits ou terminer les threads ; il offre un mcanisme dinterruption coopratif permettant de faciliter lannulation, mais cest vous de construire les protocoles dannulation et de les utiliser de faon cohrente. FutureTask et le framework Executor simplient la construction de tches et de services annulables.

8
Pools de threads
Le Chapitre 6 a introduit le framework dexcution des tches et a montr quil simplie la gestion du cycle de vie des tches et des threads et quil permet de dissocier facilement la soumission des tches de la politique dexcution. Le Chapitre 7 a prsent quelques-uns des dtails pineux du cycle de vie des services, lis lutilisation de ce framework dans les applications relles. Ce chapitre sintresse aux options avances de conguration et de rglage des pools de threads, dcrit les dangers auxquels il faut prendre garde lorsque lon utilise le framework dexcution des tches et fournit quelques exemples plus labors de lutilisation dExecutor.

8.1

Couplage implicite entre les tches et les politiques dexcution

Nous avons prtendu plus haut que le framework Executor sparait la soumission des tches de leur excution. Comme beaucoup de tentatives de dcoupler des processus complexes, notre afrmation tait un peu exagre. Bien que le framework Executor offre une souplesse non ngligeable pour la spcication et la modication des politiques dexcution, toutes les tches ne sont pas compatibles avec toutes les politiques dexcution. Les types de tches qui exigent des politiques dexcution spciques sont :
m

Les tches dpendantes. La plupart des tches qui se comportent correctement sont indpendantes : elles ne dpendent ni du timing, ni du rsultat, ni des effets de bord dautres tches. Lorsque lon excute des tches indpendantes dans un pool de thread, on peut faire varier librement la taille du pool et sa conguration sans rien affecter dautre que les performances. En revanche, lorsque lon soumet un pool des tches qui dpendent dautres tches, on cre implicitement des contraintes sur la politique dexcution qui doivent tre soigneusement gres pour viter des problmes de vivacit (voir la section 8.1.1).

172

Structuration des applications concurrentes

Partie II

Les tches qui exploitent le connement aux threads. Les excuteurs monothreads sengagent plus sur la concurrence que les pools de threads. Comme ils garantissent que les tches ne sexcuteront pas en parallle, il ny a pas besoin de se soucier de la thread safety du code des tches. Les objets pouvant tre conns au thread de la tche, celles conues pour sexcuter dans ce thread peuvent donc accder ces objets sans synchronisation, mme si ces ressources ne sont pas thread-safe. Ceci forme un couplage implicite entre la tche et la politique dexcution les tches demandent leur excuteur dtre monothread1. En ce cas, si vous remplacez un Executor monothread par un pool de threads, vous risquez de perdre cette thread safety. Les tches sensibles au temps de rponse. Les applications graphiques sont sensibles au temps de rponse : les utilisateurs napprcient pas quil scoule un long dlai entre un clic sur un bouton et son effet. Soumettre une longue tche un excuteur monothread ou plusieurs longues tches un pool ayant peu de threads peut dtriorer la ractivit du service gr par cet Executor. Les tches qui utilisent ThreadLocal. ThreadLocal autorise chaque thread possder sa propre "version" prive dune variable. Cependant, les excuteurs peuvent rutiliser les threads en fonction de leurs besoins. Les implmentations standard d Executor peuvent supprimer les threads inactifs lorsque la demande est faible et en ajouter de nouveaux lorsque la demande saccrot ; elles peuvent galement remplacer un thread par un autre si une tche lance une exception non contrle. Il ne faut donc utiliser ThreadLocal dans les threads de pool que si la valeur locale au thread a une dure de vie limite celle dune tche ; il ne faut pas sen servir dans les threads de pool pour transmettre des valeurs entre les tches.

Les pools de threads fonctionnent mieux lorsque les tches sont homognes et indpendantes. Mlanger des tches longues et courtes risque en effet d"embouteiller" le pool, sauf si ce dernier est trs grand ; soumettre des tches dpendant dautres tches risque de provoquer un interblocage (deadlock), sauf si le pool nest pas born. Heureusement, les requtes adresses aux applications serveur classiques serveurs web, de courrier, de chiers correspondent gnralement ces critres.
Les caractristiques de certaines tches ncessitent ou excluent une politique dexcution prcise. Les tches dpendant dautres tches exigent que le pool de threads soit sufsamment important pour que les tches ne soient jamais places en attente ni rejetes ; les tches qui exploitent le connement au thread exigent une excution squentielle. Il faut documenter ces exigences pour que les dveloppeurs qui reprendront le code ne perturbent pas sa thread safety ni sa vivacit en remplaant sa politique dexcution par une autre qui serait incompatible.

1. Cette exigence nest pas si forte que cela : il suft de sassurer que les tches ne sexcutent pas en parallle et fournissent une synchronisation sufsante pour que les effets mmoire de lune soient visibles dans la suivante cest prcisment la garantie quoffre newSingleThreadExecutor.

Chapitre 8

Pools de threads

173

8.1.1

Interblocage par famine de thread

Des tches qui dpendent dautres tches et qui sexcutent dans un pool de threads risquent de sinterbloquer. Dans un excuteur monothread, une tche qui en soumet une autre au mme excuteur et qui attend son rsultat provoquera toujours un interblocage : la seconde tche attendra en effet dans la le dattente que la premire se termine, mais cette dernire ne pourra jamais nir puisquelle attend le rsultat de la seconde. Il peut se passer le mme phnomne dans des pools plus grands si tous les threads qui sexcutent sont bloqus en attente dautres tches encore dans la le dattente. Cest ce que lon appelle un interblocage par famine de thread ; il peut intervenir chaque fois quune tche du pool lance une attente bloquante non borne sur une ressource ou une condition qui ne peut russir que grce laction dune autre tche du pool lattente de la valeur de retour ou de leffet de bord dune autre tche, par exemple, sauf si lon peut garantir que le pool est sufsamment grand. La classe ThreadDeadlock du Listing 8.1 illustre un interblocage par famine de thread. RenderPageTask soumet deux tches supplmentaires lExecutor pour rcuprer len-tte et le pied de page, elle traite le corps de la page, attend la n des tches header et footer puis combine len-tte, le corps et le pied de page dans la page nale. Avec un excuteur monothread, ThreadDeadlock produira toujours un interblocage. De mme, des tches qui se coordonnent entre elles avec une barrire pourraient galement provoquer un interblocage par famine de thread si le pool nest pas assez grand.
Listing 8.1 : Interblocage de tches dans un Executor monothread. Ne le faites pas.
public class ThreadDeadlock { ExecutorService exec = Executors.newSingleThreadExecutor (); public class RenderPageTask implements Callable<String> { public String call() throws Exception { Future<String> header, footer; header = exec.submit(new LoadFileTask("header.html")); footer = exec.submit(new LoadFileTask("footer.html")); String page = renderBody(); // Interblocage la tche attend la fin de la sous-tche. return header.get() + page + footer.get(); } } }

chaque fois que vous soumettez des tches non indpendantes un Executor, vous risquez un interblocage par famine de thread et vous devez prciser dans le code ou dans le chier de conguration de cet Executor toutes les informations sur la taille du pool et les contraintes de conguration.

Outre les ventuelles bornes explicites sur la taille dun pool de threads, il peut galement exister des limites implicites provenant de contraintes sur dautres ressources. Si une application utilise un pool de dix connexions JDBC et que chaque tche ait besoin

174

Structuration des applications concurrentes

Partie II

dune connexion une base de donnes, par exemple, cest comme si le pool de threads navait que dix threads car les tches excdentaires se bloqueront en attente dune connexion. 8.1.2 Tches longues

Les pools de threads peuvent rencontrer des problmes de ractivit si les tches peuvent se bloquer pendant de longues priodes, mme sil ny a pas dinterblocage. Les tches longues peuvent embouteiller un pool de threads, ce qui augmentera le temps de rponse mme pour les tches courtes. Si la taille du pool est trop petite par rapport au nombre attendu de tches longues, tous les threads du pool niront par excuter des tches longues et la ractivit sen ressentira. Une technique pouvant attnuer les effets dsagrables des tches longues consiste faire en sorte que les tches utilisent des attentes temporises pour les ressources au lieu dattentes illimites. La plupart des mthodes bloquantes de la bibliothque standard, comme Thread.join(), BlockingQueue.put(), CountDownLatch.await() et Selector .select(), existent en versions la fois temporise et non temporise. Lorsque le dlai dattente expire, on peut considrer que la tche a chou et il sagit alors de lannuler ou de la replacer en le dattente pour quelle soit rexcute plus tard. Ceci garantit que chaque tche nira par avancer, soit vers une n normale, soit vers un chec, permettant ainsi de librer des threads pour les tches qui peuvent se terminer plus rapidement. Si un pool de threads est souvent rempli de tches bloques, cela peut galement vouloir dire quil est trop petit.

8.2

Taille des pools de threads

La taille idale dun pool de threads dpend des types de tches qui seront soumis et des caractristiques du systme sur lequel il est dploy. Les tailles des pools sont rarement codes en dur ; elles devraient plutt tre indiques par un mcanisme de conguration ou calcules dynamiquement partir de Runtime.availableProcessors(). Le calcul de la taille des pools de threads nest pas une science exacte mais, heureusement, il suft dviter les valeurs "trop grandes" ou "trop petites". Si un pool est trop grand, les threads lutteront pour des ressources processeur et mmoire peu abondantes, ce qui provoquera une utilisation plus forte de la mmoire et un ventuel puisement des ressources. Sil est trop petit, le dbit sen ressentira puisque des processeurs ne seront pas utiliss pour effectuer le travail disponible. Pour choisir une taille correcte, vous devez connatre votre environnement informatique, votre volume des ressources et la nature de vos tches. De combien de processeurs le systme de dploiement dispose-t-il ? Quelle est la taille de sa mmoire ? Est-ce que les tches effectuent principalement des calculs, des E/S ou un mlange des deux ? A-t-on besoin de ressources rares, comme une connexion JDBC ? Si lon a des catgories

Chapitre 8

Pools de threads

175

diffrentes de tches avec des comportements trs diffrents, il est prfrable dutiliser plusieurs pools de threads pour que chacun puisse tre rgl en fonction de sa charge de travail. Pour les tches de calcul intensif, un systme Ncpu processeurs est utilis de faon optimale avec un pool de Ncpu+1 threads (les threads de calcul intensif pouvant aussi avoir une faute de page ou se mettre en pause pour une raison ou une autre, un thread "supplmentaire" empche que des cycles processeurs soient inutiliss lorsque cela arrive). Les tches qui contiennent galement des E/S ou dautres oprations bloquantes ont besoin dun pool plus grand puisque les threads ne seront pas tous disponibles tout moment. Pour choisir une taille correcte, vous devez estimer le rapport du temps dattente des tches par rapport leur temps de calcul ; cette estimation na pas besoin dtre prcise et peut tre obtenue en analysant le droulement du programme. La taille peut galement tre ajuste en lanant lapplication avec diffrentes tailles de pools et en observant lutilisation des processeurs. tant donn les dnition suivantes : Ncpu = nombre de processeurs Ucpu = Utilisation souhaite des processeurs, 0 Ucpu 1 W/C = rapport du temp dattente par rapport au temps de calcul La taille de pool optimale pour que les processeurs atteignent lutilisation souhaite est : Nthreads = Ncpu Ucpu (1 + W/C) Vous pouvez connatre le nombre de processeurs laide de la classe Runtime :
int N_CPUS = Runtime.getRuntime().availableProcessors ();

Les cycles processeurs ne sont, bien sr, pas la seule ressource que vous pouvez grer avec les pools de threads. Les autres ressources qui peuvent entrer en ligne de compte pour les tailles des pools sont la mmoire, les descripteurs de chiers et de sockets et les connexions aux bases de donnes. Le calcul des contraintes de taille associes ces types de ressources est plus facile : il suft dadditionner le nombre de ces ressources exig par chaque tche et de diviser par la quantit disponible. Le rsultat sera une limite suprieure de la taille du pool. Lorsque des tches demandent une ressource en pool, comme des connexions des bases de donnes, la taille du pool de threads et celle du pool de ressources saffectent mutuellement. Si chaque tche demande une connexion, la taille effective du pool de thread est limite par celle du pool de connexion. De mme, si les seuls consommateurs des connexions sont des tches du pool, la taille effective du pool de connexions est limite par celle du pool de threads.

176

Structuration des applications concurrentes

Partie II

8.3

Conguration de ThreadPoolExecutor

ThreadPoolExecutor fournit limplmentation de base des excuteurs renvoys par les mthodes fabriques newCachedThreadPool(), newFixedThreadPool() et newScheduled ThreadExecutor() de Executors. Cest une implmentation souple et robuste dun pool, qui est galement trs adaptable.

Si la politique dexcution par dfaut ne vous convient pas, vous pouvez instancier un ThreadPoolExecutor au moyen de son constructeur et ladapter vos besoins ; consultez le code source de Executors pour voir les politiques dexcution des congurations par dfaut et les utiliser comme point de dpart. ThreadPoolExecutor dispose de plusieurs constructeurs, le plus gnral est prsent dans le Listing 8.2.
Listing 8.2 : Constructeur gnral de ThreadPoolExecutor.
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { ... }

8.3.1

Cration et suppression de threads

La cration et la suppression des threads dpendent des tailles normale et maximale du pool, ainsi que du temps de maintien. La taille normale est la taille souhaite ; limplmentation tente de maintenir le pool cette taille, mme sil ny a aucune tche excuter1, et ne crera pas plus de threads tant que la le dattente nest pas pleine 2. La taille maximale du pool est la limite suprieure du nombre de threads pouvant tre actifs simultanment. Un thread qui est rest inactif pendant plus longtemps que le temps de maintien devient candidat la suppression et peut tre termin si la taille courante du pool dpasse sa taille normale.
1. Lorsquun ThreadPoolExecutor est cr, les threads initiaux sont lancs non pas immdiatement mais mesure que les tches sont soumises, moins que vous nappeliez prestartAllCoreThreads(). 2. Les dveloppeurs sont parfois tents dutiliser une taille normale non nulle pour que les threads soient supprims et nempchent pas la JVM de sarrter, mais cela peut provoquer un comportement trange avec les pools de threads qui nutilisent pas une SynchronousQueue comme le dattente (ce qui est le cas de newCachedThreadPool). Si le pool a dj sa taille normale, ThreadPoolExecutor ne cre un nouveau thread que si la le dattente est pleine : les tches soumises un pool ayant une le avec une capacit quelconque et une taille normale de zro ne sexcuteront pas tant que la le nest pas pleine, ce qui nest gnralement pas ce que lon souhaite. Avec Java 6, allowCoreThreadTimeOut permet de demander que tous les pools de thread aient un dlai dexpiration ; vous pouvez activer cette fonctionnalit avec une taille normale de zro si vous voulez obtenir un pool de threads avec une le dattente borne, mais qui supprimera quand mme tous les threads lorsquil ny a rien faire.

Chapitre 8

Pools de threads

177

En congurant la taille normale du pool et le temps de maintien, vous pouvez encourager le pool rclamer les ressources utilises par les threads inactifs an de les rendre disponibles pour le travail utile (comme dhabitude, cest un compromis : supprimer les threads inactifs ajoute une latence supplmentaire due la cration des threads si ceux-ci doivent tre plus tard crs lorsque la demande augmente). La fabrique newFixedThreadPool() xe les tailles normale et maximale la taille de pool demande, ce qui donne leffet dun dlai dexpiration inni ; la fabrique newCached ThreadPool() xe la taille maximale du pool Integer.MAX_VALUE et sa taille normale zro avec un dlai dexpiration de 1 minute, ce qui a pour effet de crer un pool de threads qui peut stendre indniment et qui se rtrcira nouveau lorsque la demande chutera. Dautres combinaisons sont possibles en utilisant le constructeur explicite de ThreadPoolExecutor. 8.3.2 Gestion des tches en attente

Les pools de threads borns limitent le nombre de tches pouvant sexcuter simultanment (les excuteurs monotches sont un cas particulier puisquils garantissent que les tches ne sexcuteront jamais en parallle, ce qui permet dobtenir une thread safety au moyen du connement au thread). Dans la section 6.1.2, nous avons vu quune cration dun nombre illimit de threads pouvait conduire une instabilit et avons rgl ce problme en utilisant un pool de threads de taille xe au lieu de crer un nouveau thread pour chaque requte. Cependant, ce nest quune solution partielle : mme si cela est moins probable, lapplication peut quand mme se retrouver court de ressources en cas de forte charge. En effet, si la frquence darrive des nouvelles requtes est suprieure celle de leur traitement, ces requtes seront places en le dattente. Avec un pool de threads, elles attendront dans une le de Runnable gre par lExecutor au lieu dtre places dans une le de threads concourant pour laccs au processeur. Reprsenter une tche en attente par un objet Runnable et un nud dune liste est certainement beaucoup moins coteux que la reprsenter par un thread, mais le risque dpuiser les ressources reste quand mme prsent si les clients peuvent envoyer des requtes au serveur plus vite quil ne peut les grer ; en effet, les requtes arrivent souvent par paquet, mme lorsque leur frquence moyenne est assez stable. Les les dattente peuvent aider attnuer les pics temporaires de tches mais, si celles-ci continuent darriver trop rapidement, vous devrez ralentir leur frquence darrive pour viter de tomber en panne de mmoire 1. Avant mme
1. Ceci est analogue au contrle du ux dans les communications sur le rseau : vous pouvez placer un certain volume de donnes dans un tampon, mais vous devrez quand mme trouver un moyen pour que lautre extrmit cesse denvoyer des donnes ou supprimer les donnes excdentaires en esprant que lexpditeur les retransmettra lorsque vous ne serez plus aussi satur.

178

Structuration des applications concurrentes

Partie II

dtre court de mmoire, le temps de rponse empirera progressivement mesure que la le des tches grossira.
ThreadPoolExecutor vous permet de fournir une BlockingQueue pour stocker les tches en attente dexcution. Il existe trois approches de base pour la mise en attente des tches : utiliser une le non borne, une le borne ou un transfert synchrone. Le choix de la le inue sur dautres paramtres de conguration, comme la taille du pool.

Par dfaut, newFixedThreadPool() et newSingleThreadExecutor() utilisent une le LinkedBlockingQueue non borne : les tches sont places dans cette le si tous les threads sont occups mais elle peut grossir sans limite si les tches continuent darriver plus vite quelles ne peuvent tre traites. Une stratgie de gestion des ressources plus stable consiste utiliser une liste borne, comme une ArrayBlockingQueue ou une LinkedBlockingQueue ou une PriorityBlocking Queue bornes. Ces les empchent lpuisement des ressources mais posent le problme du devenir des nouvelles tches lorsque la le est pleine (dans la section 8.3.3, nous verrons quil existe un certain nombre de politiques de saturation possibles pour rgler ce problme). Avec une le dattente borne, la taille de la le et celle du pool doivent tre mutuellement adaptes : une grande le associe un petit pool permet de rduire la consommation mmoire, lutilisation du processeur et le basculement de contexte au prix dune contrainte sur le dbit. Pour les pools trs gros ou non borns, vous pouvez galement compltement vous passer dune le dattente et transmettre directement les tches des producteurs aux threads en utilisant une SynchronousQueue qui, en ralit, nest absolument pas une le mais est un mcanisme permettant de grer les changes entre threads. Pour placer un lment dans une SynchronousQueue, il faut quil y ait dj un autre thread en attente du transfert. Si aucun thread nattend et si la taille courante du pool est infrieure la taille maximale, ThreadPoolExecutor cre un nouveau thread ; sinon la tche est rejete selon la politique de saturation. Lutilisation dun transfert direct est plus efcace car la tche peut tre passe directement au thread qui lexcutera plutt qutre place dans une le o elle devra attendre quun thread vienne la rcuprer. SynchronousQueue nest possible que si le pool nest pas born ou si lon peut rejeter les tches excdentaires. La mthode fabrique newCachedThreadPool() utilise une SynchronousQueue. Lutilisation dune le dattente FIFO comme LinkedBlockingQueue ou ArrayBlocking Queue force les tches tre lances dans leur ordre darrive. Pour avoir plus de contrle sur leur ordre dexcution, vous pouvez utiliser une PriorityBlockingQueue qui les ordonnera en fonction de leurs priorits respectives, dnies selon lordre naturel (si les tches implmentent Comparable) ou par un Comparator.

Chapitre 8

Pools de threads

179

La mthode fabrique newCachedThreadPool() est un bon choix par dfaut pour un Executor, car les performances obtenues sont meilleures que celles dun pool de threads de taille xe 1. Ce dernier est un choix correct lorsque lon veut limiter le nombre de tches concurrentes pour grer les ressources, comme dans le cas dune application serveur qui reoit des requtes provenant de clients rseaux et qui, sinon, pourrait se retrouver surcharge.

Borner le pool de threads ou la le dattente nest souhaitable que lorsque les tches sont indpendantes. Lorsque les tches dpendent dautres tches, les pools de threads ou les les bornes peuvent provoquer des interblocages par famine de threads ; utilisez plutt une conguration de pool non born, comme newCachedThreadPool()2. 8.3.3 Politiques de saturation

La politique de saturation entre en jeu lorsquune le dattente borne est remplie. Pour un ThreadPoolExecutor, vous pouvez la modier en appelant la mthode setRejected ExecutionHandler() (elle est galement utilise lorsquune tche est soumise un Executor qui a t arrt). Il existe plusieurs versions de RejectedExecutionHandler pour mettre en uvre les diffrentes politiques de saturation, AbortPolicy, CallerRunsPolicy, DiscardPolicy et DiscardOldestPolicy. La politique par dfaut, abort, force execute() lancer lexception non contrle RejectedExecutionException qui peut tre capture par lappelant pour implmenter sa propre gestion du dpassement en fonction de ses besoins. La politique discard supprime en silence la nouvelle tche soumise si elle ne peut pas tre mise dans la le dattente ; la politique discard-oldest supprime la prochaine tche qui serait excute et tente de resoumettre la nouvelle tche (si la le dattente est une le a priorits, cela supprimera llment le plus prioritaire et cest la raison pour laquelle la combinaison de cette politique avec une le a priorits nest pas un bon choix). La politique caller-runs implmente une forme de ralentissement qui ne supprime pas de tche et ne lance pas dexception, mais qui tente de ralentir le ux des nouvelles tches en renvoyant une partie du travail lappelant. La nouvelle tche soumise sexcute non pas dans un thread du pool mais dans le thread qui appelle execute(). Si nous avions modi notre exemple WebServer pour quil utilise une le dattente borne et la politique caller-runs, la nouvelle tche se serait excute dans le thread
1. Cette diffrence de performances provient de lutilisation de SynchronousQueue au lieu de Linked BlockingQueue. Avec Java 6, SynchronousQueue a t remplace par un nouvel algorithme non bloquant qui amliore le dbit dun facteur de trois par rapport limplmentation de SynchronousQueue en Java 5 (Scherer et al., 2006). 2. Une autre conguration possible pour les tches qui soumettent dautres tches et qui attendent leurs rsultats consiste utiliser un pool de threads born, une SynchronousQueue comme le dattente et la politique de saturation caller-runs.

180

Structuration des applications concurrentes

Partie II

principal au cours de lappel execute() si tous les threads du pool taient occups et la le dattente, remplie. Comme cela prendrait srement un certain temps, le thread principal ne peut pas soumettre dautres tches pendant un petit moment, an de donner le temps aux threads du pool de rattraper leur retard. Le thread principal nappellera pas non plus accept() pendant ce laps de temps et les requtes entrantes seront donc places dans la le dattente de la couche TCP au lieu de ltre dans lapplication. Si la surcharge persiste, la couche TCP nit par dcider quelle a plac en attente sufsamment de demandes de connexions et commence supprimer galement des requtes. mesure que le serveur devient surcharg, cette surchage est graduellement dplace vers lextrieur du pool de threads vers la le dattente, vers lapplication et vers la couche TCP et, enn, vers le client , ce qui permet une dgradation plus progressive en cas de forte charge. Choisir une politique de saturation ou effectuer des modications sur la politique dexcution peut se faire au moment de la cration de lExecutor. Le Listing 8.3 montre la cration dun pool de threads de taille xe avec une politique de saturation caller-runs.
Listing 8.3 : Cration dun pool de threads de taille fixe avec une file borne et la politique de saturation caller-runs.
ThreadPoolExecutor executor = new ThreadPoolExecutor(N_THREADS, N_THREADS, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(CAPACITY)); executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy ());

Il nexiste pas de politique de saturation prdnie pour faire en sorte que execute() se bloque lorsque la le dattente est pleine. Cependant, on peut obtenir le mme rsultat en utilisant un Semaphore pour borner la frquence dinjection des tches, comme dans le Listing 8.4. Avec cette approche, on utilise une le non borne (il ny a aucune raison de borner la fois la taille de la le et la frquence dinjection) et on limite le smaphore pour quil soit gal la taille du pool plus le nombre de tches en attente que lon veut autoriser, car le smaphore borne la fois le nombre de tches en cours et en attente dexcution.
Listing 8.4 : Utilisation dun Semaphore pour ralentir la soumission des tches.
@ThreadSafe public class BoundedExecutor { private final Executor exec; private final Semaphore semaphore; public BoundedExecutor(Executor exec, int bound) { this.exec = exec; this.semaphore = new Semaphore(bound); } public void submitTask(final Runnable command) throws InterruptedException { semaphore.acquire();

Chapitre 8

Pools de threads

181

try { exec.execute(new Runnable() { public void run() { try { command.run(); } finally { semaphore.release(); } } }); } catch (RejectedExecutionException e) { semaphore.release(); } } }

8.3.4

Fabriques de threads

chaque fois quil a besoin de crer un thread, un pool de threads utilise une fabrique de thread (voir Listing 8.5). La fabrique par dfaut cre un thread non dmon sans conguration spciale. En indiquant une fabrique spcique, vous pouvez adapter la conguration des threads du pool. ThreadFactory na quune seule mthode, newThread(), qui est appele chaque fois quun pool de threads doit crer un nouveau thread.
Listing 8.5 : Interface ThreadFactory.
public interface ThreadFactory { Thread newThread(Runnable r); }

Il y a un certain nombre de raisons dutiliser une fabrique de thread personnalise : vous pouvez, par exemple, vouloir prciser un UncaughtExceptionHandler pour les threads du pool ou crer une instance dune classe Thread adapte vos besoins, qui inscrit des informations de dbogage dans un journal. Vous pourriez galement vouloir modier la priorit (ce qui nest gnralement pas souhaitable, comme on lexplique dans la section 10.3.1) ou positionner ltat dmon (ce qui nest pas non plus conseill, comme on la indiqu dans la section 7.4.2) des threads du pool. Vous pouvez aussi simplement vouloir donner des noms plus signicatifs aux threads an de simplier linterprtation des traces et des journaux derreur. La classe MyThreadFactory du Listing 8.6 prsente une fabrique de threads personnaliss. Elle cre un nouvel objet MyAppThread en passant au constructeur un nom spcique au pool an de le reprer plus facilement dans les traces et les journaux derreur. MyAppThread peut galement tre utilise ailleurs dans lapplication pour que tous les threads puissent bncier de ses fonctionnalits de dbogage.

182

Structuration des applications concurrentes

Partie II

Listing 8.6 : Fabrique de threads personnaliss.


public class MyThreadFactory implements ThreadFactory { private final String poolName; public MyThreadFactory(String poolName) { this.poolName = poolName; } public Thread newThread(Runnable runnable) { return new MyAppThread(runnable, poolName); } }

La partie intressante se situe dans la classe MyAppThread, qui est prsente dans le Listing 8.7 et qui permet de donner un nom au thread, de mettre en place un Uncaught ExceptionHandler personnalis qui crit un message sur un Logger, de grer des statistiques sur le nombre de threads crs et supprims et qui inscrit ventuellement un message de dbogage dans le journal lorsquun thread est cr ou termin.
Listing 8.7 : Classe de base pour les threads personnaliss.
public class MyAppThread extends Thread { public static final String DEFAULT_NAME = "MyAppThread"; private static volatile boolean debugLifecycle = false; private static final AtomicInteger created = new AtomicInteger(); private static final AtomicInteger alive = new AtomicInteger(); private static final Logger log = Logger.getAnonymousLogger(); public MyAppThread(Runnable r) { this(r, DEFAULT_NAME); } public MyAppThread(Runnable runnable, String name) { super(runnable, name + "-" + created.incrementAndGet ()); setUncaughtExceptionHandler( new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { log.log(Level.SEVERE, "UNCAUGHT in thread " + t.getName(), e); } }); } public void run() { // Copie lindicateur debug pour garantir la cohrence de sa valeur. boolean debug = debugLifecycle; if (debug) log.log(Level.FINE, "Created "+getName()); try { alive.incrementAndGet (); super.run(); } finally { alive.decrementAndGet (); if (debug) log.log(Level.FINE, "Exiting "+getName()); } } public public public public } static static static static int getThreadsCreated() { return created.get(); } int getThreadsAlive() { return alive.get(); } boolean getDebug(){ return debugLifecycle; } void setDebug(booleanb) { debugLifecycle = b; }

Chapitre 8

Pools de threads

183

Si votre application utilise une politique de scurit pour accorder des permissions du code particulier, vous pouvez utiliser la mthode fabrique privilegedThreadFactory() de Executors pour construire votre fabrique de threads. Celle-ci cre des threads de pool ayant les mmes permissions, AccessControlContext et contextClassLoader, comme le thread crant la privilegedThreadFactory. Sinon les threads crs par le pool hritent des permissions du client qui appelle execute() ou submit() lorsquun nouveau thread est ncessaire, ce qui peut provoquer des exceptions troublantes, lies la scurit. 8.3.5 Personnalisation de ThreadPoolExecutor aprs sa construction

La plupart des options passes aux constructeurs de ThreadPoolExecutor (notamment les tailles normale et maximale du pool, le temps de maintien, la fabrique de threads et le gestionnaire dexcution rejete) peuvent galement tre modies aprs la construction de lobjet via des mthodes modicatrices. Si lExecutor a t cr au moyen de lune des mthodes fabriques de Executors (sauf newSingleThreadExecutor), il est possible de le transtyper en ThreadPoolExecutor an davoir accs aux mthodes modicatrices, comme dans le Listing 8.8.
Listing 8.8 : Modification dun Executor cr avec les mthodes fabriques standard.
ExecutorService exec = Executors.newCachedThreadPool(); if (exec instanceof ThreadPoolExecutor ) ((ThreadPoolExecutor) exec).setCorePoolSize (10); else throw new AssertionError("Oops, bad assumption");

Executors fournit la mthode fabrique unconfigurableExecutorService(), qui prend en paramtre un ExecutorService existant et lenveloppe dans un objet ne proposant que les mthodes de ExecutorService an quil ne puisse plus tre congur. la diffrence des implmentations utilisant des pools, newSingleThreadExecutor() renvoie un objet ExecutorService envelopp de cette faon au lieu dun ThreadPoolExecutor brut. Bien quun excuteur monothread soit en fait implment comme un pool ne contenant quun seul thread, il garantit galement que les tches ne sexcuteront pas en parallle. Si un code mal inspir tentait daugmenter la taille du pool dun excuteur monothread, cela ruinerait la smantique recherche pour lexcution.

Vous pouvez utiliser cette technique avec vos propres excuteurs pour empcher la modication de la politique dexcution : si vous exposez un ExecutorService du code et que vous ne soyez pas sr que ce code nessaie pas de le modier, vous pouvez envelopper lExecutorService dans un unconfigurableExecutorService.

8.4

Extension de ThreadPoolExecutor

La classe ThreadPoolExecutor a t conue pour tre tendue et fournit plusieurs "points dattache" (hooks) destins tre rednis par les sous-classes beforeExecute(), after Execute() et terminated() pour tendre le comportement de ThreadPoolExecutor.

184

Structuration des applications concurrentes

Partie II

Les mthodes beforeExecute() et afterExecute() sont appeles dans le thread qui excute la tche et peuvent servir ajouter une journalisation, mesurer le temps dexcution, surveiller les tches ou rcolter des statistiques. afterExecute() est appele chaque fois que la tche se termine en quittant normalement run() ou en lanant une Exception (elle nest pas appele si la tche se termine cause dune Error). Si beforeExecute() lance une RuntimeException, la tche nest pas excute et afterExecute() nest pas appele. La mthode terminated() est appele lorsque le pool de threads a termin son processus dextinction, lorsque toutes les tches se sont termines et que tous les threads se sont teints. Elle peut servir librer les ressources alloues par lExecutor au cours de son cycle de vie, envoyer des notications, inscrire des informations dans le journal ou terminer la rcolte des statistiques. 8.4.1 Exemple : ajout de statistiques un pool de threads

La classe TimingThreadPool du Listing 8.9 montre un pool de threads personnalis qui utilise beforeExecute(), afterExecute() et terminated() pour ajouter une journalisation et une rcolte de statistiques. Pour mesurer le temps dexcution dune tche, beforeExecute() doit enregistrer le temps de dpart et le stocker un endroit o after Execute() pourra le rcuprer. Ces hooks dexcution tant appels dans le thread qui excute la tche, une valeur place dans un ThreadLocal par beforeExecute() peut tre relue par afterExecute(). TimingThreadPool utilise deux AtomicLong pour mmoriser le nombre total de tches traites et le temps total de traitement ; il se sert galement de la mthode terminated() pour inscrire un message dans le journal contenant le temps moyen de la tche.
Listing 8.9 : Pool de threads tendu par une journalisation et une mesure du temps.
public class TimingThreadPool extends ThreadPoolExecutor { private final ThreadLocal<Long> startTime = new ThreadLocal<Long>(); private final Logger log = Logger.getLogger("TimingThreadPool "); private final AtomicLong numTasks = new AtomicLong(); private final AtomicLong totalTime = new AtomicLong(); protected void beforeExecute(Thread t, Runnable r) { super.beforeExecute(t, r); log.fine(String.format("Thread %s: start %s", t, r)); startTime.set(System.nanoTime()); } protected void afterExecute(Runnable r, Throwable t) { try { long endTime = System.nanoTime(); long taskTime = endTime - startTime.get(); numTasks.incrementAndGet (); totalTime.addAndGet(taskTime); log.fine(String.format("Thread %s: end %s, time=%dns", t, r, taskTime)); } finally {

Chapitre 8

Pools de threads

185

super.afterExecute(r, t); } } protected void terminated() { try { log.info(String.format("Terminated: avg time=%dns", totalTime.get() / numTasks.get())); } finally { super.terminated(); } } }

8.5

Paralllisation des algorithmes rcursifs

Les exemples dafchage de page de la section 6.3 passaient par une srie damliorations tendant rechercher un paralllisme exploitable. La premire tentative tait entirement squentielle ; la deuxime utilisait deux threads mais continuait tlcharger les images en squence ; la dernire traitait chaque tlchargement dimage comme une tche distincte an dobtenir un paralllisme plus fort. Les boucles dont les corps contiennent des calculs non triviaux ou qui effectuent des E/S potentiellement bloquantes sont gnralement de bonnes candidates la paralllisation du moment que les itrations sont indpendantes. Si vous avez une boucle dont les itrations sont indpendantes et que vous nayez pas besoin quelles se terminent toutes avant de continuer, vous pouvez utiliser un Executor pour la tranformer en boucle parallle, comme le montrent les mthodes process Sequentially() et processInParallel() du Listing 8.10.
Listing 8.10 : Transformation dune excution squentielle en excution parallle.
void processSequentially(List<Element> elements) { for (Element e : elements) process(e); } void processInParallel(Executor exec, List<Element> elements) { for (final Element e : elements) exec.execute(new Runnable() { public void run() { process(e); } }); }

Un appel processInParallel() redonne le contrle plus rapidement quun appel processSequentially() car il se termine ds que toutes les tches ont t mises en attente dans lExecutor au lieu dattendre quelles se terminent toutes. Si vous voulez soumettre un ensemble de tches et attendre quelles soient toutes termines, vous pouvez utiliser la mthode ExecutorService.invokeAll() ; pour rcuprer les rsultats ds

186

Structuration des applications concurrentes

Partie II

quils sont disponibles, vous pouvez vous servir dun CompletionService comme dans la classe Renderer du Listing 6.15.
Les itrations dune boucle squentielle peuvent tre mises en parallle si elles sont mutuellement indpendantes et si le travail effectu dans chaque itration du corps de la boucle est sufsamment signicatif pour justier le cot de la gestion dune nouvelle tche.

La paralllisation des boucles peut galement sappliquer certaines constructions rcursives ; dans un algorithme rcursif, il y a souvent des boucles squentielles qui peuvent tre paralllises comme dans le Listing 8.10. Le cas le plus simple est lorsque chaque itration na pas besoin des rsultats des itrations quelle invoque rcursivement. La mthode sequentialRecursive() du Listing 8.11, par exemple, effectue un parcours darbre en profondeur dabord en ralisant un calcul sur chaque nud puis en plaant le rsultat dans une collection. Sa version transforme, parallelRecursive(), effectue le mme parcours mais en soumettant une tche pour calculer le rsultat de chaque nud, au lieu de le calculer chaque fois quelle visite un nud.
Listing 8.11 : Transformation dune rcursion terminale squentielle en rcursion parallle.
public<T> void sequentialRecursive(List<Node<T>> nodes, Collection<T> results) { for (Node<T> n: nodes) { results.add(n.compute()); sequentialRecursive(n.getChildren(), results); } } public<T> void parallelRecursive(final Executor exec, List<Node<T>> nodes, final Collection<T> results) { for (final Node<T> n : nodes) { exec.execute(new Runnable() { public void run() { results.add(n.compute()); } }); parallelRecursive(exec, n.getChildren(), results); } }

Lorsque parallelRecursive() se termine, chaque nud de larbre a t visit (le parcours est toujours squentiel : seuls les appels compute() sont excuts en parallle) et le rsultat pour chacun deux a t plac dans la le de lExecutor. Les appelants de cette mthode peuvent attendre tous les rsultats en crant en Executor spcique au parcours et utiliser shutdown() et awaitTermination() comme on le montre dans le Listing 8.12.

Chapitre 8

Pools de threads

187

Listing 8.12 : Attente des rsultats calculs en parallle.


public<T> Collection<T> getParallelResults (List<Node<T>> nodes) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool (); Queue<T> resultQueue = new ConcurrentLinkedQueue <T>(); parallelRecursive(exec, nodes, resultQueue); exec.shutdown(); exec.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); return resultQueue; }

8.5.1

Exemple : un framework de jeu de rexion

Une application amusante de cette technique permet de rsoudre des jeux de rexion consistant trouver une suite de transformations pour passer dun tat initial un tat nal, comme dans les jeux du taquin1, du solitaire, etc. On dnit un "puzzle" comme une combinaison dune position initiale, dune position nale et dun ensemble de rgles qui dnit les coups autoriss. Cet ensemble est form de deux parties : un calcul de la liste des coups autoriss partir dune position donne et un calcul du rsultat de lapplication dun coup une position. Linterface Puzzle du Listing 8.13 dcrit notre abstraction : les paramtres de type P et M reprsentent les classes pour une position et un coup. partir de cette interface, nous pouvons crire un simple rsolveur squentiel qui parcourt lespace du puzzle jusqu trouver une solution ou jusqu ce que cet espace soit puis.
Listing 8.13 : Abstraction pour les jeux de type "taquin".
public interface Puzzle<P, M> { P initialPosition (); boolean isGoal(P position); Set<M> legalMoves(P position); P move(P position, M move); }

Dans le Listing 8.14, Node reprsente une position qui a t atteinte via une suite de coups ; elle contient une rfrence au coup qui a cr la position et une rfrence au Node prcdent. En remontant les liens partir dun Node, nous pouvons donc reconstruire la suite de coups qui a men la position courante.
Listing 8.14 : Nud pour le framework de rsolution des jeux de rflexion.
@Immutable static class Node<P, M> { final P pos; final M move; final Node<P, M> prev; Node(P pos, M move, Node<P, M> prev) {...}

1. Voir http://www.puzzleworld.org/SlidingBlockPuzzles.

188

Structuration des applications concurrentes

Partie II

Listing 8.14 : Nud pour le framework de rsolution des jeux de rflexion. (suite)
List<M> asMoveList() { List<M> solution = new LinkedList<M>(); for (Node<P, M> n = this; n.move != null; n = n.prev) solution.add(0, n.move); return solution; } }

La classe SequentialPuzzleSolver du Listing 8.15 est un rsolveur squentiel qui effectue un parcours en profondeur dabord de lespace du puzzle. Elle se termine lorsquelle a trouv une solution (qui nest pas ncessairement la plus courte). La rcriture de ce rsolveur pour exploiter le paralllisme nous permettrait de calculer les coups suivants et dvaluer si le but a t atteint en parallle car le processus dvaluation dun coup est presque indpendant de lvaluation des autres coups (nous crivons "presque" car les tches partagent un tat modiable, comme lensemble des positions dj tudies). Si lon dispose de plusieurs processeurs, cela peut rduire le temps ncessaire la dcouverte dune solution.
Listing 8.15 : Rsolveur squentiel dun puzzle.
public class SequentialPuzzleSolver <P, M> { private final Puzzle<P, M> puzzle; private final Set<P> seen = new HashSet<P>(); public SequentialPuzzleSolver (Puzzle<P, M> puzzle) { this.puzzle = puzzle; } public List<M> solve() { P pos = puzzle.initialPosition (); return search(new Node<P, M>(pos, null, null)); } private List<M> search(Node<P, M> node) { if (!seen.contains(node.pos)) { seen.add(node.pos); if (puzzle.isGoal(node.pos)) return node.asMoveList(); for (M move : puzzle.legalMoves(node.pos)) { P pos = puzzle.move(node.pos, move); Node<P, M> child = new Node<P, M>(pos, move, node); List<M> result = search(child); if (result != null) return result; } } return null; } static class Node<P, M> { /* Listing 8.14 */ } }

La classe ConcurrentPuzzleSolver du Listing 8.16 utilise une classe interne Solver Task qui tend Node et implmente Runnable. Lessentiel du travail a lieu dans run() : valuation des prochaines positions possibles, suppression des positions dj tudies,

Chapitre 8

Pools de threads

189

valuation du but atteint (par cette tche ou une autre) et soumission des positions non tudies un Executor. Pour viter les boucles sans n, la version squentielle mmorisait un Set des positions dj parcourues alors que ConcurrentPuzzleSolver utilise un ConcurrentHashMap. Ce choix garantit une thread safety et vite les situations de comptition inhrentes la mise jour conditionnelle dune collection partage en utilisant putIfAbsent() pour ajouter de faon atomique une position uniquement si elle nest pas dj connue. Pour stocker ltat de la recherche, ConcurrentPuzzleSolver utilise la le dattente interne du pool de threads la place de la pile dappels.
Listing 8.16 : Version parallle du rsolveur de puzzle.
public class ConcurrentPuzzleSolver <P, M> { private final Puzzle<P, M> puzzle; private final ExecutorService exec; private final ConcurrentMap <P, Boolean> seen; final ValueLatch<Node<P, M>> solution = new ValueLatch<Node<P, M>>(); ... public List<M> solve() throws InterruptedException { try { P p = puzzle.initialPosition (); exec.execute(newTask(p, null, null)); // bloque jusqu ce quune solution soit trouve Node<P, M> solnNode = solution.getValue(); return (solnNode == null) ? null : solnNode.asMoveList(); } finally { exec.shutdown(); } } protected Runnable newTask(P p, M m, Node<P,M> n) { return new SolverTask(p, m, n); } class SolverTask extends Node<P, M> implements Runnable { ... public void run() { if (solution.isSet() || seen.putIfAbsent(pos, true) != null) return; // already solved or seen this position if (puzzle.isGoal(pos)) solution.setValue(this); else for (M m : puzzle.legalMoves(pos)) exec.execute(newTask(puzzle.move(pos, m), m, this)); } } }

Lapproche parallle change galement une forme de limitation contre une autre qui peut tre plus adapte au domaine du problme. La version squentielle effectuant une recherche en profondeur dabord, cette recherche est limite par la taille de la pile. La version parallle effectue, quant elle, une recherche en largeur dabord et est donc affranchie du problme de la taille de la pile (bien quelle puisse quand mme se retrouver

190

Structuration des applications concurrentes

Partie II

court de mmoire si lensemble des positions parcourir ou dj parcourues dpasse la quantit de mmoire disponible). Pour stopper la recherche lorsque lon a trouv une solution, il faut pouvoir dterminer si un thread a dj trouv une solution. Si nous voulons accepter la premire solution trouve, nous devons galement ne mettre jour la solution que si aucune autre tche nen a dj trouv une. Ces exigences dcrivent une sorte de loquet (voir la section 5.5.1) et, notamment, un loquet de rsultat partiel. Nous pourrions aisment construire un tel loquet grce aux techniques prsentes au Chapitre 14, mais il est souvent plus simple et plus able dutiliser les classes existantes de la bibliothque au lieu des mcanismes de bas niveau du langage. La classe ValueLatch du Listing 8.17 utilise un objet Count DownLatch pour fournir le comportement souhait du loquet et se sert du verrouillage pour garantir que la solution nest trouve quune seule fois.
Listing 8.17 : Loquet de rsultat partiel utilis par ConcurrentPuzzleSolver.
@ThreadSafe public class ValueLatch<T> { @GuardedBy("this") private T value = null; private final CountDownLatch done = new CountDownLatch (1); public boolean isSet() { return (done.getCount() == 0); } public synchronized void setValue(T newValue) { if (!isSet()) { value = newValue; done.countDown(); } } public T getValue() throws InterruptedException { done.await(); synchronized (this) { return value; } } }

Chaque tche consulte dabord le loquet de la solution et sarrte si une solution a dj t trouve. Le thread principal doit attendre jusqu ce quune solution ait t trouve ; la mthode getValue() de ValueLatch se bloque jusqu ce quun thread ait initialis la valeur. ValueLatch fournit un moyen de mmoriser une valeur de sorte que seul le premier appel xe en ralit la valeur ; les appelants peuvent tester si elle a t initialise et se bloquer en attente si ce nest pas le cas. Au premier appel de setValue(), la solution est mise jour et lobjet CountDownLatch est dcrment, ce qui libre le thread rsolveur principal dans getValue(). Le premier thread trouver une solution met galement n lExecutor pour empcher quil accepte dautres tches. Pour viter de traiter RejectedExecutionException, le

Chapitre 8

Pools de threads

191

gestionnaire dexcution rejete doit tre congur pour supprimer les tches soumises. De cette faon, toutes les tches non termines sexcuteront jusqu leur n et toutes les nouvelles tentatives dexcuter de nouvelles tches choueront en silence, permettant ainsi lexcuteur de se terminer (si les tches mettent trop de temps sexcuter, nous pourrions les interrompre au lieu de les laisser nir).
ConcurrentPuzzleSolver ne gre pas trs bien le cas o il ny a pas de solution : si tous les coups possibles et toutes les solutions ont t valus sans trouver de solution, solve() attend indniment dans lappel getSolution(). La version squentielle se terminait lorsquelle avait puis lespace de recherche, mais faire en sorte que des programmes parallles se terminent peut parfois tre plus difcile. Une solution possible consiste mmoriser le nombre de tches rsolveur actives et dinitialiser la solution null lorsque ce nombre tombe zro ; cest ce que lon fait dans le Listing 8.18.
Listing 8.18 : Rsolveur reconnaissant quil ny a pas de solution.
public class PuzzleSolver<P,M> extends ConcurrentPuzzleSolver <P,M> { ... private final AtomicInteger taskCount = new AtomicInteger (0); protected Runnable newTask(P p, M m, Node<P,M> n) { return new CountingSolverTask (p, m, n); } class CountingSolverTask extends SolverTask { CountingSolverTask(P pos, M move, Node<P, M> prev) { super(pos, move, prev); taskCount.incrementAndGet (); } public void run() { try { super.run(); } finally { if (taskCount.decrementAndGet () == 0) solution.setValue(null); } } } }

Trouver la solution peut galement prendre plus de temps que ce que lon est prt attendre ; nous pourrions alors imposer plusieurs conditions de terminaison au rsolveur. Lune delles est une limite de temps, qui peut aisment tre ralise en implmentant une version temporise de getValue() dans ValueLatch (cette version utiliserait la version temporise de await()) et en arrtant lExecutor en mentionnant lchec si le dlai imparti getValue() a expir. Une autre mesure spcique aux jeux de rexion consiste ne rechercher quun certain nombre de positions. On peut galement fournir un mcanisme dannulation et laisser le client choisir quand arrter la recherche.

192

Structuration des applications concurrentes

Partie II

Rsum
Le framework Executor est un outil puissant et souple pour excuter des tches en parallle. Il offre un certain nombre doptions de conguration, comme les politiques de cration et de suppression des tches, la gestion des tches en attente et ce quil convient de faire des tches excdentaires. Il fournit galement des mthodes dinterception (hooks) permettant dtendre son comportement. Cela dit, comme avec la plupart des frameworks puissants, certaines combinaisons de conguration ne fonctionnent pas ensemble ; certains types de tches exigent des politiques dexcutions spciques et certaines combinaisons de paramtres de conguration peuvent produire des rsultats curieux.

9
Applications graphiques
Si vous avez essay dcrire une application graphique, mme simple, avec Swing, vous savez que ce type de programme a ses propres problmes avec les threads. Pour maintenir la thread safety, certaines tches doivent sexcuter dans le thread des vnements de Swing, mais vous ne pouvez pas lancer des tches longues dans ce thread sous peine dobtenir une interface utilisateur peu ractive. En outre, les structures de donnes de Swing ntant pas thread-safe, vous devez vous assurer de les conner au thread des vnements. Quasiment tous les toolkits graphiques, dont Swing et AWT, sont implments comme des sous-systmes monothreads dans lesquels toute lactivit de linterface graphique est conne dans un seul thread. Si vous navez pas lintention dcrire un programme entirement monothread, certaines activits devront sexcuter en partie dans le thread de lapplication et en partie dans le thread des vnements. Comme la plupart des autres bogues lis aux threads, votre programme ne se plantera pas ncessairement immdiatement si cette division est mal faite ; il peut se comporter bizarrement sous certaines conditions quil est souvent difcile didentier. Bien que les frameworks graphiques soient des sous-systmes monothreads, votre application peut ne pas ltre et vous devrez quand mme vous soucier des problmes de thread lorsque vous crirez du code graphique.

9.1

Pourquoi les interfaces graphiques sont-elles monothreads ?

Par le pass, les applications graphiques taient monothreads et les vnements de linterface taient traits partir dune "boucle dvnements". Les frameworks graphiques modernes utilisent un modle qui est peu diffrent puisquils crent un thread ddi (EDT, pour Event Dispatch Thread) an de grer les vnements de linterface. Les frameworks graphiques monothreads ne sont pas propres Java : Qt, NextStep, Cocoa de Mac OS, X-Window et de nombreux autres sont galement monothreads. Ce nest

194

Structuration des applications concurrentes

Partie II

pourtant pas faute davoir essay ; il y a eu de nombreuses tentatives de frameworks graphiques multithreads qui, cause des problmes persistants avec les situations de comptition et les interblocages, sont toutes arrives au modle dune le dvnements monothread dans lequel un thread ddi rcupre les vnements dans une le et les transmet des gestionnaires dvnements dnis par lapplication (au dpart, AWT a essay dautoriser un plus grand degr daccs multithread et la dcision de rendre Swing monothread est largement due lexprience dAWT). Les frameworks graphiques multithreads ont tendance tre particulirement sensibles aux interblocages, en partie cause de la mauvaise interaction entre le traitement des vnements dentre et le modle orient objet utilis pour reprsenter les composants graphiques, quel quil soit. Les actions de lutilisateur ont tendance "remonter" du systme dexploitation lapplication un clic de souris est dtect par le SE, transform en vnement "clic souris" par le toolkit et nit par tre dlivr un couteur de lapplication sous la forme dun vnement de plus haut niveau, comme un vnement "bouton press". Les actions inities par lapplication, en revanche, "descendent" de lapplication vers le SE la modication de la couleur de fond dun composant est demande dans lapplication, puis est envoye une classe de composant spcique et nit dans le SE, qui se charge de lafchage. Combiner cette tendance des activits accder aux mmes objets graphiques dans un ordre oppos avec la ncessit de rendre chaque objet thread-safe est la meilleure recette pour obtenir un ordre de verrouillage incohrent menant un interblocage (voir Chapitre 10). Cest exactement ce que quasiment tout effort de dveloppement dun toolkit graphique a redcouvert par exprience. Un autre facteur provoquant les interblocages dans les frameworks graphiques multithreads est la prvalence du patron modle-vue-contrleur (MVC). Le regroupement des interactions utilisateur en objets coopratifs modle, vue et contrleur simplie beaucoup limplmentation des applications graphiques mais accrot le risque dun ordre de verrouillage incohrent. En effet, le contrleur peut appeler le modle, qui prvient la vue que quelque chose a t modi. Mais le contrleur peut galement appeler la vue, qui peut son tour appeler le modle pour interroger son tat. L encore, on obtient un ordre de verrouillage incohrent avec le risque dinterblocage qui en dcoule. Dans son blog1, le vice-prsident de Sun, Graham Hamilton, rsume parfaitement ces ds en expliquant pourquoi le toolkit graphique multithread est lun des "rves dus" rcurrents de linformatique :
"Je crois que vous pouvez programmer correctement avec des toolkits graphiques multithreads si le toolkit a t soigneusement conu, si le toolkit expose en moult dtails sa mthode de verrouillage, si vous tes
1. Voir http://weblogs.java.net/blog/kgh/archive/2004/10.

Chapitre 9

Applications graphiques

195

trs malin, trs prudent et que vous ayez une vision globale de la structure complte du toolkit. Si lun de ces critres fait dfaut, les choses marcheront pour lessentiel mais, de temps en temps, lapplication se gera ( cause des interblocages) ou se comportera bizarrement ( cause des situations de comptition). Cette approche multithreads marche surtout pour les personnes qui ont t intimement impliques dans le dveloppement du toolkit. Malheureusement, je ne crois pas que cet ensemble de critres soit adapt une utilisation commerciale grande chelle. Le rsultat sera que les programmeurs normaux auront tendance produire des applications qui ne sont pas tout fait ables et quil ne sera pas du tout vident den trouver la cause : ils seront donc trs mcontents et frustrs et utiliseront les pires mots pour qualier le pauvre toolkit innocent."

Les frameworks graphiques monothreads ralisent la thread safety via le connement au thread ; on naccde tous les objets de linterface graphique, y compris les composants visuels et les modles de donnes, qu partir du thread des vnements. Cela ne fait, bien sr, que repousser une partie du problme de la thread safety vers le dveloppeur de lapplication, qui doit sassurer que ces objets sont correctements conns. 9.1.1 Traitement squentiel des vnements

Les interfaces graphiques sarticulent autour du traitement dvnements prcis, comme les clics de souris, les pressions de touche ou lexpiration de dlais. Les vnements sont un type de tche ; du point de vue structurel, la mcanique de gestion des vnements fournie par AWT et Swing est semblable un Executor. Comme il ny a quun seul thread pour traiter les tches de linterface graphique, celles-ci sont gres en squence une tche se termine avant que la suivante ne commence et il ny a jamais recouvrement de deux tches. Cet aspect facilite lcriture du code des tches car il ny a pas besoin de se soucier des interfrences entre elles. Linconvnient du traitement squentiel des tches est que, si lune dentre elles met du temps sexcuter, les autres devront attendre quelle se soit termine. Si ces autres tches sont charges de rpondre une saisie de lutilisateur ou de fournir une rponse visuelle, lapplication semblera ge. Si une tche longue sexcute dans le thread des vnements, lutilisateur ne peut mme pas cliquer sur "Annuler" puisque lcouteur de ce bouton ne sera pas appel tant que la tche longue ne sest pas termine. Les tches qui sexcutent dans le thread des vnements doivent donc redonner rapidement le contrle ce thread. Une tche longue, comme une vrication orthographique dun gros document, une recherche dans le systme de chiers ou la rcupration dune ressource sur le rseau, doit tre lance dans un autre thread an de redonner rapidement le contrle au thread des vnements. La mise jour dune jauge de progression pendant quune longue tche sexcute ou la cration dun effet visuel lorsquelle sest

196

Structuration des applications concurrentes

Partie II

termine, en revanche, doit sexcuter dans le thread des vnments. Tout ceci peut devenir assez rapidement compliqu. 9.1.2 Connement aux threads avec Swing

Tous les composants de Swing (comme JButton et JTable) et les objets du modle de donnes (comme TableModel et TreeModel) tant conns au thread des vnements, tout code qui les utilise doit sexcuter dans ce thread. Les objets de linterface graphique restent cohrents non grce la synchronisation mais grce au connement. Lavantage est que les tches qui sexcutent dans le thread des vnements nont pas besoin de se soucier de synchronisation lorsquelles accdent aux objets de prsentation ; linconvnient est que lon ne peut pas du tout accder ces objets en dehors de ce thread.
Rgle monothread de Swing : les composants et modles Swing devraient tre crs, modis et consults uniquement partir du thread des vnements.

Comme avec toutes les rgles, il y a quelques exceptions. Un petit nombre de mthodes Swing peuvent tre appeles en toute scurit partir de nimporte quel thread ; elles sont clairement identies comme thread-safe dans Javadoc. Les autres exceptions cette rgle sont :
m

SwingUtilities.isEventDispatchThread(), qui dtermine si le thread courant est le thread des vnements ; SwingUtilities.invokeLater(), qui planie une tche Runnable pour quelle sexcute dans le thread des vnements (appelable partir de nimporte quel thread) ; SwingUtilities.invokeAndWait(), qui planie une tche Runnable pour quelle sexcute dans le thread des vnements et bloque le thread courant jusqu ce quelle soit termine (appelable uniquement partir dun thread nappartenant pas linterface graphique) ;

les mthodes pour mettre en attente un rafchage ou une demande de revalidation dans la le des vnements (appelables partir de nimporte quel thread) ; les mthodes pour ajouter et ter des couteurs (appelables partir de nimporte quel thread, mais les couteurs seront toujours invoqus dans le thread des vnements).

Les mthodes invokeLater() et invokeAndWait() fonctionnent beaucoup comme un Executor. En fait, comme le montre le Listing 9.1, limplmentation des mthodes de SwingUtilities lies aux threads est triviale si lon se sert dun Executor monothread. Ce nest pas de cette faon que SwingUtilities est rellement implmente car Swing est antrieur au framework Executor, mais elle le serait srement de cette manire si Swing tait crit aujourdhui.

Chapitre 9

Applications graphiques

197

Listing 9.1 : Implmentation de SwingUtilities laide dun Executor.


public class SwingUtilities { private static final ExecutorService exec = Executors.newSingleThreadExecutor(new SwingThreadFactory ()); private static volatile Thread swingThread; private static class SwingThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { swingThread = new Thread(r); return swingThread; } } public static boolean isEventDispatchThread () { return Thread.currentThread() == swingThread; } public static void invokeLater(Runnable task) { exec.execute(task); } public static void invokeAndWait(Runnable task) throws InterruptedException , InvocationTargetException { Future f = exec.submit(task); try { f.get(); } catch (ExecutionException e) { throw new InvocationTargetException (e); } } }

Le thread des vnements Swing peut tre vu comme un Executor monothread qui traite les tches en attente dans la le des vnements. Comme pour les pools de threads, le thread excutant meurt parfois et est remplac par un nouveau, mais cela devrait tre transparent du point de vue des tches. Une excution squentielle, monothread, est une politique dexcution raisonnable lorsque les tches sont courtes, que la prvision de lordonnancement na pas dimportance ou quil est impratif que les tches ne sexcutent pas en parallle. La classe GuiExecutor du Listing 9.2 est un Executor qui dlgue les tches Swing Utilities pour les excuter. Elle pourrait galement tre implmente laide dautres frameworks graphiques ; SWT, par exemple, fournit la mthode Display.asyncExec(), qui ressemble SwingUtilities.invokeLater().
Listing 9.2 : Executor construit au-dessus de SwingUtilities.
public class GuiExecutor extends AbstractExecutorService { // Les singletons ont un constructeur priv et une fabrique publique. private static final GuiExecutor instance = new GuiExecutor(); private GuiExecutor() { } public static GuiExecutor instance() { return instance; }

198

Structuration des applications concurrentes

Partie II

Listing 9.2 : Executor construit au-dessus de SwingUtilities. (suite)


public void execute(Runnable r) { if (SwingUtilities.isEventDispatchThread ()) r.run(); else SwingUtilities.invokeLater(r); } // Plus des implmentations triviales des mthodes du cycle de vie. }

9.2

Tches courtes de linterface graphique

Dans une application graphique, les vnements partent du thread des vnements et remontent vers les couteurs fournis par lapplication, qui effectueront probablement certains traitements affectant les objets de prsentation. Dans le cas des tches simples et courtes, toute laction peut rester dans le thread des vnements ; pour les plus longues, une partie du traitement devrait tre dcharge vers un autre thread. Dans le cas le plus simple, le connement des objets de prsentation au thread des vnements est totalement naturel. Le Listing 9.3, par exemple, cre un bouton dont la couleur change de faon alatoire lorsquil est enfonc. Lorsque lutilisateur clique sur le bouton, le toolkit dlivre un vnement ActionEvent dans le thread des vnements destination de tous les couteurs daction enregistrs. En rponse, lcouteur daction choisit une couleur et modie celle du fond du bouton. Lvnement part donc du toolkit graphique, est dlivr lapplication et celle-ci modie linterface graphique en rponse laction de lutilisateur. Le contrle na jamais besoin de quitter le thread des vnements, comme le montre la Figure 9.1.
Listing 9.3 : couteur dvnement simple.
final Random random = new Random(); final JButton button = new JButton("Change Color"); ... button.addActionListener(new ActionListener () { public void actionPerformed(ActionEvent e) { button.setBackground(new Color(random.nextInt())); } });

EDT Figure 9.1

clic souris

vnement action

couteur daction

modification de la couleur

Flux de contrle dun simple clic sur un bouton.

Chapitre 9

Applications graphiques

199

Cet exemple trivial reprsente la majorit des interactions entre les applications et les toolkits graphiques. Tant que les tches sont courtes et naccdent quaux objets de linterface graphique (ou dautres objets de lapplication conns au thread ou thread-safe), vous pouvez presque totalement ignorer les problmes lis aux threads et tout faire partir du thread dvnement : tout se passera bien. La Figure 9.2 illustre une version un peu plus complique de ce scnario car elle implique un modle de donnes formel comme TableModel ou TreeModel. Swing divise la plupart des composants visuels en deux objets, un modle et une vue. Les donnes afcher rsident dans le modle et les rgles gouvernant lafchage se situent dans la vue. Les objets modles peuvent dclencher des vnements indiquant que les donnes du modle ont t modies et les vues ragissent ces vnements. Lorsquelle reoit un vnement indiquant que les donnes du modle ont peut-tre chang, la vue interroge le modle pour obtenir les nouvelles donnes et met lafchage jour. Dans un couteur de bouton qui modie le contenu dune table, lcouteur mettrait jour le modle et appellerait lune des mthodes fireXxx() qui, son tour, invoquerait les couteurs du modle de la table de la vue, qui modieraient la vue. L encore, le contrle ne quitte jamais le thread des vnements (les mthodes fireXxx() de Swing appelant toujours directement les couteurs au lieu dajouter un nouvel vnement dans la le des vnements, elles ne doivent tre appeles qu partir du thread des vnements).

EDT

clic souris

vnement action

couteur daction

modification du modle de la table

vnement table modifie Figure 9.2

couteur de table

modification de la vue de la table

Flux de contrle avec des objets modle et vue spars.

9.3

Tches longues de linterface graphique

Si toutes les tches taient courtes (et si la partie non graphique de lapplication ntait pas signicative), lapplication entire pourrait sexcuter dans le thread des vnements et lon naurait absolument pas besoin de prter attention aux threads. Cependant, les applications graphiques sophistiques peuvent excuter des tches qui durent plus longtemps que ce que lutilisateur est prt attendre ; cest le cas, par exemple, dune vrication orthographique, dune compilation en arrire-plan ou dune rcupration de ressources distantes. Ces tches doivent donc sexcuter dans un autre thread pour que linterface graphique puisse rester ractive pendant leur droulement.

200

Structuration des applications concurrentes

Partie II

Swing facilite lexcution dune tche dans le thread des vnements, mais (avant Java 6) il ne fournit aucun mcanisme pour aider les tches excuter leur code dans dautres threads. Cependant, nous navons pas besoin que Swing nous aide, ici, car nous pouvons crer notre propre Executor pour traiter les tches longues. Un pool de threads en cache est un bon choix pour ce type de tche ; les applications graphiques ne lanant que trs rarement un grand nombre de tches longues, il y a peu de risques que le pool grossisse sans limite. Nous commencerons par une tche simple qui ne supporte pas lannulation ni dindication de progression et qui ne modie pas linterface graphique lorsquelle est termine, puis nous ajouterons ces fonctionnalits une une. Le Listing 9.4 montre un couteur daction li un composant visuel qui soumet une longue tche un Executor. Malgr les deux couches de classes internes, il est assez facile de comprendre comment une tche graphique lance une tche : lcouteur daction de linterface est appel dans le thread des vnements et soumet lexcution dune tche Runnable au pool de threads.
Listing 9.4 : Liaison dune tche longue un composant visuel.
ExecutorService backgroundExec = Executors.newCachedThreadPool (); ... button.addActionListener(new ActionListener () { public void actionPerformed(ActionEvent e) { backgroundExec .execute(new Runnable() { public void run() { doBigComputation (); } }); }});

Cet exemple sort la longue tche du thread des vnements en sen "dbarrassant", ce qui nest pas trs pratique car on attend gnralement un effet visuel lorsque la tche sest termine. Ici, on ne peut pas accder aux objets de prsentation partir du thread en arrire-plan ; quand elle sest termine, la tche doit donc soumettre une autre tche pour quelle sexcute dans le thread des vnements an de modier linterface utilisateur. Le Listing 9.5 montre la faon vidente de le faire, qui commence quand mme devenir complique car nous avons maintenant besoin de trois couches de classes internes. Lcouteur daction dsactive dabord le bouton et lui associe un texte indiquant quun calcul est en cours, puis il soumet une tche lexcuteur en arrire-plan. Lorsque cette tche se termine, il met en le dattente une autre tche pour quelle sexcute dans le thread des vnements, ce qui ractive le bouton et restaure le texte de son label.
Listing 9.5 : Tche longue avec effet visuel.
button.addActionListener(new ActionListener () { public void actionPerformed(ActionEvent e) { button.setEnabled(false); label.setText("busy"); backgroundExec .execute(new Runnable() { public void run() { try { doBigComputation (); } finally {

Chapitre 9

Applications graphiques

201

GuiExecutor.instance().execute(new Runnable() { public void run() { button.setEnabled(true); label.setText("idle"); } }); } } }); } });

La tche dclenche lorsque le bouton est enfonc est compose de trois sous-tches squentielles dont lexcution alterne entre le thread des vnements et le thread en arrire-plan. La premire sous-tche met jour linterface utilisateur pour indiquer quune opration longue a commenc et lance la seconde dans un thread en arrire-plan. Lorsquelle est termine, cette seconde sous-tche met en le dattente la troisime sous-tche pour quelle sexcute nouveau dans le thread des vnements. Cette sorte de "ping-pong de threads" est typique de la gestion des tches longues dans les applications graphiques. 9.3.1 Annulation

Toute tche qui dure assez longtemps dans un autre thread peut inciter lutilisateur lannuler. Nous pourrions implmenter directement lannulation laide dune interruption du thread, mais il est bien plus simple dutiliser Future, qui a t conue pour grer les tches annulables. Lorsque lon appelle cancel() sur un objet Future dont mayInterruptIfRunning vaut true, limplmentation de Future interrompt le thread de la tche sil est en cours dexcution. Si la tche a t crite pour pouvoir rpondre aux interruptions, elle peut donc se terminer prcocment si elle est annule. Le Listing 9.6 montre une tche qui interroge lindicateur dinterruption du thread et se termine rapidement sil est positionn.
Listing 9.6 : Annulation dune tche longue.
Future<?> runningTask = null; // confine au thread ... startButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (runningTask = null) { runningTask = backgroundExec .submit(new Runnable() { public void run() { while (moreWork()) { if (Thread.interrupted()) { cleanUpPartialWork(); break; } doSomeWork(); } } }); }; }});

202

Structuration des applications concurrentes

Partie II

Listing 9.6 : Annulation dune tche longue. (suite)


cancelButton.addActionListener(new ActionListener () { public void actionPerformed(ActionEvent event) { if (runningTask != null) runningTask.cancel(true); }});

runningTask tant conne au thread des vnements, il ny a pas besoin de synchronisation pour la modier ou la consulter et lcouteur du bouton de dmarrage garantit quune seule tche en arrire-plan sexcutera un instant donn. Cependant, il serait prfrable dtre prvenu lorsque la tche se termine pour que, par exemple, le bouton dannulation puisse tre dsactiv. Cest ce que nous allons implmenter dans la section qui suit.

9.3.2

Indication de progression et de terminaison

Lutilisation dun objet Future pour reprsenter une tche longue simplie beaucoup limplmentation de lannulation et FutureTask dispose galement dun hook done() qui facilite la notication de la terminaison dune tche puisquil est appel lorsque le Callable en arrire-plan sest termin. En faisant en sorte que done() lance une tche de terminaison dans le thread des vnements, nous pouvons donc construire une classe BackgroundTask fournissant un hook onCompletion() qui sera appel dans le thread des vnements, comme on le montre dans le Listing 9.7.
Listing 9.7 : Classe de tche en arrire-plan supportant lannulation, ainsi que la notication de terminaison et de progression.
abstract class BackgroundTask <V> implements Runnable, Future<V> { private final FutureTask<V> computation = new Computation(); private class Computation extends FutureTask<V> { public Computation() { super(new Callable<V>() { public V call() throws Exception { return BackgroundTask.this.compute(); } }); } protected final void done() { GuiExecutor.instance().execute(new Runnable() { public void run() { V value = null; Throwable thrown = null; boolean cancelled = false; try { value = get(); } catch (ExecutionException e) { thrown = e.getCause(); } catch (CancellationException e) { cancelled = true; } catch (InterruptedException consumed) { } finally { onCompletion(value, thrown, cancelled); } }; }); } }

Chapitre 9

Applications graphiques

203

protected void setProgress(final int current, final int max) { GuiExecutor.instance().execute(new Runnable() { public void run() { onProgress(current, max); } }); } // Appele dans le thread en arrire-plan protected abstract V compute() throws Exception; // Appele dans le thread des vnements protected void onCompletion(V result, Throwable exception, boolean cancelled) { } protected void onProgress(int current, int max) { } // Autres mthodes de Future dlgues Computation }

BackgroundTask permet galement dindiquer la progression de la tche. La mthode compute() peut appeler setProgress(), qui indique la progression par une valeur numrique. onProgress() sera appele partir du thread des vnements, qui peut mettre jour linterface utilisateur pour montrer visuellement la progression.

Pour implmenter une BackgroundTask il suft dcrire compute(), qui est appele dans le thread en arrire-plan. On peut galement rednir onCompletion() et onProgress(), qui, elles, sont invoques dans le thread des vnements. Faire reposer BackgroundTask sur FutureTask permet galement de simplier lannulation car, au lieu dinterroger lindicateur dinterruption du thread, compute() peut se contenter dappeler Future.isCancelled(). Le Listing 9.8 reformule lexemple du Listing 9.6 en utilisant BackgroundTask.
Listing 9.8 : Utilisation de BackgroundTask pour lancer une tche longue et annulable.
public void runInBackground(final Runnable task) { startButton.addActionListener(new ActionListener () { public void actionPerformed(ActionEvent e) { class CancelListener implements ActionListener { BackgroundTask<?> task; public void actionPerformed(ActionEvent event) { if (task != null) task.cancel(true); } } final CancelListener listener = new CancelListener (); listener.task = new BackgroundTask <Void>() { public Void compute() { while (moreWork() && !isCancelled()) doSomeWork(); return null; } public void onCompletion(boolean cancelled, String s, Throwable exception) { cancelButton.removeActionListener(listener); label.setText("done"); } }; cancelButton.addActionListener(listener); backgroundExec.execute(task); } }); }

204

Structuration des applications concurrentes

Partie II

9.3.3

SwingWorker

Nous avons construit un framework simple utilisant FutureTask et Executor pour excuter des tches longues dans des threads en arrire-plan sans dtriorer la ractivit de linterface graphique. Ces techniques peuvent sappliquer nimporte quel framework graphique monothread, elles ne sont pas rserves Swing. Cependant, la plupart des fonctionnalits dveloppes ici annulation, notication de terminaison et indication de progression sont fournies par la classe SwingWorker de Swing. Plusieurs versions de cette classe ont t publies dans la Swing Connection et dans le didacticiel Java et une version mise jour a t intgre Java 6.

9.4

Modles de donnes partages

Les objets de prsentation de Swing, qui comprennent les objets de modles de donnes comme TableModel ou TreeModel, sont conns dans le thread des vnements. Dans les programmes graphiques simples, tout ltat modiable est contenu dans les objets de prsentation et le seul thread qui existe part le thread des vnements est le thread principal. Dans ces programmes, il est facile de respecter la rgle monothread : ne manipulez pas les composants du modle de donnes ou de la prsentation partir du thread principal. Les programmes plus complexes peuvent utiliser dautres threads pour dplacer les donnes partir de ou vers une zone de stockage persistante, comme un systme de chiers ou une base de donnes, sans pour autant compromettre la ractivit de lapplication. Dans le cas le plus simple, les donnes du modle de donnes sont saisies par lutilisateur ou charges statiquement partir dun chier ou dune autre source au dmarrage de lapplication, auquel cas les donnes ne seront jamais touches par un autre thread que celui des vnements. Cependant, lobjet du modle de prsentation nest parfois quune vue vers une autre source de donnes, comme une base de donnes, un systme de chiers ou un service distant. Dans ce cas, plusieurs threads manipuleront srement les donnes mesure quelles entrent et sortent de lapplication. Vous pourriez, par exemple, afcher le contenu dun systme de chiers distant en utilisant un modle arborescent. Pour cela, vous ne souhaiterez pas numrer tout le systme de chiers avant de pouvoir afcher toute larborescence : cela serait trop long et consommerait trop de mmoire. Au lieu de cela, larborescence peut tre remplie mesure que les nuds sont dvelopps. Mme lnumration dun petit rpertoire situ sur un volume distant pouvant prendre du temps, il est prfrable de leffectuer dans une tche en arrire-plan. Lorsque celle-ci se termine, il faut trouver un moyen de stocker les donnes dans le modle arborescent, ce qui peut se faire au moyen dun modle arborescent thread-safe, en "poussant" les donnes de la tche en arrire-plan vers le thread des vnements. Pour cela, on poste une tche avec invokeLater() ou lon fait en sorte que le thread des vnements regarde si les donnes sont disponibles.

Chapitre 9

Applications graphiques

205

9.4.1

Modles de donnes thread-safe

Tant que la ractivit nest pas trop affecte par les blocages, le problme des threads multiples manipulant les donnes peut se rgler par un modle de donnes thread-safe. Si ce modle supporte des accs concurrents prcis, le thread des vnements et ceux en arrire-plan devraient pouvoir le partager sans perturber la ractivit. La classe Delegating VehicleTracker du Listing 4.7, par exemple, utilise un objet ConcurrentHashMap sousjacent dont les oprations de rcupration autorisent un haut degr de concurrence. Linconvnient est quil noffre pas un instantan cohrent des donnes, ce qui peut tre, ou non, une ncessit. En outre, les modles de donnes thread-safe doivent produire des vnements lorsque le modle a t modi, an que les vues puissent galement ltre. On peut parfois obtenir la thread safety, la cohrence et une bonne ractivit en utilisant un modle de donnes versionn comme CopyOnWriteArrayList [CPJ 2.2.3.3]. Un itrateur sur ce type de collection la parcourt telle quelle tait lorsquil a t cr. Cependant, ces collections noffrent de bonnes performances que lorsque le nombre des parcours est bien suprieur celui des modications, ce qui ne sera srement pas le cas avec, par exemple, une application de suivi de vhicules. Des structures de donnes versionnes plus spcialises peuvent viter ce dfaut, mais construire des structures qui la fois fournissent un accs concurrent efcace et ne mmorisent pas les anciennes versions des donnes plus longtemps quil nest ncessaire nest pas chose facile ; cela ne devrait tre envisag que si les autres approches ne sont pas utilisables. 9.4.2 Modles de donnes spars

Du point de vue de linterface graphique, les classes de Swing comme TableModel et TreeModel sont les dpts ofciels pour les donnes afcher. Cependant, ces objets modles sont souvent eux-mmes des "vues" dautres objets grs par lapplication. Un programme qui possde la fois un modle de donnes pour la prsentation et un autre pour lapplication repose sur un modle de sparation (Fowler, 2005). Dans un modle de sparation, le modle de prsentation est conn dans le thread des vnements et lautre modle, le modle partag, est thread-safe et la fois le thread des vnements et les threads de lapplication peuvent y accder. Le modle de prsentation enregistre les couteurs avec le modle partag an de pouvoir tre prvenu des mises jour. Il peut alors tre modi partir du modle partag en intgrant un instantan de ltat pertinent dans le message de mise jour ou en faisant en sorte de rcuprer directement les donnes partir du modle partag lorsquil reoit un vnement de mise jour. Lapproche par instantan est simple mais possde quelques limitations. Elle fonctionne bien quand le modle de donnes est petit, que les mises jour ne sont pas trop frquentes et que les structures des deux modles se ressemblent. Lorsque le modle de donnes a une taille importante, que les mises jour sont trs frquentes ou que lune ou lautre des deux parties de la sparation contient des informations non visibles lautre, il peut tre plus efcace denvoyer des mises jour incrmentales au lieu dinstantans entiers.

206

Structuration des applications concurrentes

Partie II

Cette approche revient srialiser les mises jour sur le modle partag et les recrer dans les threads des vnements par rapport au modle de prsentation. Un autre avantage des mises jour incrmentales est quune information plus ne sur ce qui a t modi permet damliorer la qualit perue de lafchage si un seul vhicule se dplace, nous navons pas besoin de rafcher tout lcran, mais uniquement les rgions concernes.

Un modle de sparation est un bon choix lorsquun modle de donnes doit tre partag par plusieurs threads et quimplmenter un modle thread-safe serait inadapt pour des raisons de blocage, de cohrence ou de complexit.

9.5

Autres formes de sous-systmes monothreads

Le connement au thread nest pas rserv aux interfaces graphiques : il peut tre utilis chaque fois quune fonctionnalit est implmente sous la forme dun soussystme monothread. Parfois, ce connement est impos au dveloppeur pour des raisons qui nont rien voir avec la synchronisation ou les interblocages : certaines bibliothques natives, par exemple, exigent que tout accs la bibliothque, mme son chargement avec System.loadLibrary(), ait lieu dans le mme thread. En empruntant lapproche des frameworks graphiques, on peut aisment crer un thread ddi ou un excuteur monothread pour accder cette bibliothque native et fournir un objet mandataire qui intercepte les appels lobjet conn au thread et les soumet sous forme de tches au thread ddi. Future et newSingleThreadExecutor() facilitent cette implmentation : la mthode mandataire peut soumettre la tche et immdiatement appeler Future.get() pour attendre le rsultat (si la classe qui doit tre conne un thread implmente une interface, on peut automatiser le processus consistant ce que chaque mthode soumette un objet Callable un thread en arrire-plan et attendre le rsultat en utilisant des mandataires dynamiques).

Rsum
Les frameworks graphiques sont quasiment toujours implments comme des soussystmes monothreads dans lesquels tout le code li la prsentation sexcute sous forme de tches dans un thread des vnements. Comme il ny a quun seul thread des vnements, les tches longues peuvent compromettre la ractivit de linterface et doivent donc sexcuter dans des threads en tche de fond. Les classes utilitaires comme Swing Worker ou la classe BackgroundTask que nous avons construite, qui fournissent un support pour lannulation, lindication de la progression et de la terminaison dune tche, permettent de simplier le dveloppement des tches longues qui possdent des composants graphiques et non graphiques.

III
Vivacit, performances et tests

10
viter les problmes de vivacit
Il y a souvent des conits entre la thread safety et la vivacit. On utilise le verrouillage pour garantir la premire, mais son utilisation irrchie peut causer des interblocages lis lordre des verrous. De mme, on utilise des pools de threads et des smaphores pour limiter la consommation des ressources, mais une mauvaise conception de cette limitation peut impliquer des interblocages de ressources. Les applications Java ne pouvant pas se remettre dun interblocage, il faut sassurer que lon a pris soin dcarter les situations qui peuvent les provoquer. Dans ce chapitre, nous prsenterons certaines causes des problmes de vivacit et nous verrons comment les viter.

10.1

Interblocages (deadlock)

Les interblocages sont illustrs par le problme classique, bien que peu hyginique, du "dner des philosophes". Cinq philosophes sont assis autour dune table ronde dans un restaurant chinois, il y a cinq baguettes sur la table (pas cinq paires), chacune tant place entre deux assiettes ; les philosophes alternent leurs activits entre rchir et manger. Pour manger, un philosophe doit possder les deux baguettes de chaque ct de son assiette sufsamment longtemps, puis il les repose sur la table et repart dans ses rexions. Certains algorithmes de gestion des baguettes permettent chacun de manger plus ou moins rgulirement (un philosophe affam essaiera de prendre ses deux baguettes mais, sil ny en a quune de disponible, il reposera lautre et attendra une minute avant de ressayer), dautres peuvent faire que tous les philosophes meurent de faim (chacun deux prend immdiatement la baguette situe sa gauche et attend que la droite soit disponible avant de la reposer). Cette dernire situation, o chacun possde une ressource ncessaire lautre, attend une ressource dtenue par un autre et ne relche pas la sienne tant quil nobtient pas celle quil na pas, illustre parfaitement le problme des interblocages.

210

Vivacit, performances et tests

Partie III

Lorsquun thread dtient un verrou indniment, ceux qui tentent de lobtenir seront bloqus en permanence. Si le thread A dtient le verrou V et tente dacqurir le verrou W alors quau mme moment le thread B dtient W et tente dobtenir V, les deux threads attendront indniment. Cette situation est le cas dinterblocage le plus simple (on lappelle "treinte fatale"), o plusieurs threads sont bloqus dnitivement cause dune dpendance cyclique du verrouillage (considrez les threads comme les nuds dun graphe orient dont les arcs reprsentent la relation "le thread A attend une ressource dtenue par le thread B". Si ce graphe est cyclique, alors, il y a interblocage). Les systmes de gestion de bases de donnes sont conus pour dtecter les interblocages et sen chapper. Une transaction peut acqurir plusieurs verrous qui seront dtenus jusqu ce quelle soit valide : il est donc possible, et frquent, que deux transactions se bloquent mutuellement. Sans intervention, elles attendraient donc indniment (en dtenant des verrous qui sont srement ncessaires dautres transactions). Cependant, le serveur de base de donnes ne laissera pas cette situation arriver : quand il dtecte quun ensemble de transactions sont interbloques (en recherchant les cycles dans le graphe des verrous), il choisit une victime et annule sa transaction, ce qui libre le verrou quelle dtenait et permet aux autres de continuer. Lapplication peut ensuite retenter la transaction annule, qui peut se poursuivre maintenant que les transactions concurrentes se sont termines. La JVM nest pas tout fait aussi efcace que les serveurs de bases de donnes pour rsoudre les interblocages. Lorsquun ensemble de threads Java se bloquent mutuellement, cest la n du jeu ces threads sont dnitivement hors de course. Selon ce quils faisaient, toute lapplication ou simplement lun de ses sous-systmes peut se ger, ou les performances peuvent chuter. La seule faon de remettre les choses en place est alors de larrter et de la relancer en esprant que le problme ne se reposera pas de nouveau. Comme la plupart des autres problmes de concurrence, les interblocages se manifestent rarement immdiatement. Le fait quune classe puisse provoquer un interblocage ne signie pas quelle le fera ; cest uniquement une possibilit. Lorsque les interblocages apparaissent, cest gnralement au pire moment en production et sous une charge importante. 10.1.1 Interblocages lis lordre du verrouillage La classe LeftRightDeadlock du Listing 10.1 risque de provoquer un interblocage car les mthodes leftRight() et rightLeft() prennent, respectivement, les verrous left et right. Si un thread appelle leftRight() et un autre, rightLeft(), et que leurs actions sentrelacent comme la Figure 10.1, ils se bloqueront mutuellement.

Chapitre 10

viter les problmes de vivacit

211

Listing 10.1 : Interblocage simple li lordre du verrouillage. Ne le faites pas.


// Attention : risque dinterblocage ! public class LeftRightDeadlock { private final Object left = new Object(); private final Object right = new Object(); public void leftRight() { synchronized (left) { synchronized (right) { doSomething(); } } } public void rightLeft() { synchronized (right) { synchronized (left) { doSomethingElse(); } } } }

A B

verrouille X verrouille Y

tente de verrouiller Y tente de verrouiller X

attend indfiniment attend indfiniment

Figure 10.1
Timing malheureux dans LeftRightDeadlock.

Linterblocage de LeftRightDeadlock provient du fait que les deux threads ont tent de prendre les mmes verrous dans un ordre diffrent. Sils les avaient demands dans le mme ordre, il ny aurait pas eu de dpendance cyclique entre les verrous et donc aucun interblocage. Si lon peut garantir que chaque thread qui a besoin simultanment des verrous V et W les prendra dans le mme ordre, il ny aura pas dinterblocage.
Un programme est immunis contre les interblocages lis lordre de verrouillage si tous les threads prennent les verrous dont ils ont besoin selon un ordre global bien tabli.

La vrication dun verrouillage cohrent exige une analyse globale du verrouillage dans le programme. Il ne suft pas dinspecter individuellement les extraits du code qui prennent plusieurs verrous : leftRight() et rightLeft() acquirent "correctement" les deux verrous, ce qui ne les empche pas dtre incompatibles. Avec le verrouillage, la main gauche doit savoir ce que fait la main droite.

212

Vivacit, performances et tests

Partie III

10.1.2 Interblocages dynamiques lis lordre du verrouillage Il nest pas toujours vident davoir un contrle sufsant sur lordre du verrouillage an dviter les interblocages. tudiez par exemple le code apparemment inoffensif du Listing 10.2, qui transfre des fonds dun compte un autre. Ce code verrouille les deux objets Account avant dexcuter le transfert pour sassurer que les comptes seront modis de faon atomique sans violer des invariants comme "un compte ne peut pas avoir une balance ngative".
Listing 10.2 : Interblocage dynamique li lordre du verrouillage. Ne le faites pas.
// Attention : risque dinterblocage ! public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException { synchronized (fromAccount) { synchronized (toAccount) { if (fromAccount.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAccount.debit(amount); toAccount.credit(amount); } } } }

Comment linterblocage peut-il survenir ? Il peut sembler que tous les threads prennent leurs verrous dans le mme ordre, or cet ordre dpend de celui des paramtres passs transferMoney(), qui leur tour dpendent de donnes externes. Un interblocage peut donc survenir si deux threads appellent transferMoney() en mme temps, lun transfrant de X vers Y, lautre dans le sens inverse :
A : transferMoney(monCompte, tonCompte, 10); B : transferMoney(tonCompte, monCompte, 20);

Avec un timing malheureux, A prendra le verrrou sur monCompte et attendra celui sur tonCompte, tandis que B dtiendra le verrou sur tonCompte et attendra celui sur monCompte. Ce type dinterblocage peut tre dtect de la mme faon que celui du Listing 10.1 il suft de rechercher les prises de verrous imbriques. Comme, ici, lordre des paramtres ne dpend pas de nous, il faut, pour rsoudre le problme, induire un ordre sur les verrous et toujours les prendre dans cet ordre tout au long de lapplication. Un bon moyen dinduire un ordre sur les objets consiste utiliser System.identity HashCode(), qui renvoie la valeur qui serait retourne par Object.hashCode(). Le Listing 10.3 montre une version de transferMoney qui utilise cette mthode pour induire un ordre de verrouillage. Bien quelle ajoute quelques lignes de code, elle limine lventualit dun interblocage.

Chapitre 10

viter les problmes de vivacit

213

Listing 10.3 : Induire un ordre de verrouillage pour viter les interblocages.


private static final Object tieLock = new Object(); public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException { class Helper { public void transfer() throws InsufficientFundsException { if (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException (); else { fromAcct.debit(amount); toAcct.credit(amount); } } } int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); if (fromHash < toHash) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toAcct) { synchronized (fromAcct) { new Helper().transfer(); } } } else { synchronized (tieLock) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } } }

Pour les rares cas o deux objets auraient le mme code de hachage, il faut utiliser un moyen arbitraire dordonner les prises de verrou, ce qui rintroduit la possibilit dun interblocage. Pour empcher un ordre incohrent dans ce cas prcis, on utilise donc un troisime verrou pour viter les "ex aequo". En prenant le verrou tieLock avant nimporte quel verrou Account, on garantit quun seul thread la fois prendra le risque dacqurir deux verrous dans un ordre quelconque, ce qui limine le risque dinterblocage ( condition que ce mcanisme soit utilis de faon cohrente). Si les collisions de hachages taient frquentes, cette technique pourrait devenir un goulot dtranglement pour la concurrence (exactement comme lutilisation dun seul verrou pour tout le programme) mais, ces collisions tant trs rares avec System.identityHashCode(), elle fournit la dernire touche de scurit moindre cot.

214

Vivacit, performances et tests

Partie III

Si Account a une cl unique, comparable et non modiable, comme un numro de compte bancaire, linduction dun ordre sur les verrous est encore plus facile : il suft dordonner les objets selon leurs cls, ce qui limine le recours un verrou dgalit. Vous pourriez penser que nous exagrons le risque dinterblocage car les verrous ne sont gnralement dtenus que pendant un temps trs court, mais ces interblocages posent de srieux problmes dans les vrais systmes. Une application en production peut raliser des millions de cycles verrouillage-dverrouillage par jour et il en suft dun seul qui arrive au mauvais moment pour bloquer tout le programme. En outre, mme des tests soigneux en rgime de charge peuvent ne pas dceler tous les interblocages possibles1. La classe DemonstrateDeadlock2 du Listing 10.4, par exemple, provoquera assez rapidement un interblocage sur la plupart des systmes.
Listing 10.4 : Boucle provoquant un interblocage dans une situation normale.
public class DemonstrateDeadlock { private static final int NUM_THREADS = 20; private static final int NUM_ACCOUNTS = 5; private static final int NUM_ITERATIONS = 1000000; public static void main(String[] args) { final Random rnd = new Random(); final Account[] accounts = new Account[NUM_ACCOUNTS]; for (int i = 0; i < accounts.length; i++) accounts[i] = new Account(); class TransferThread extends Thread { public void run() { for (int i=0; i<NUM_ITERATIONS ; i++) { int fromAcct = rnd.nextInt(NUM_ACCOUNTS); int toAcct = rnd.nextInt(NUM_ACCOUNTS); DollarAmount amount = new DollarAmount(rnd.nextInt(1000)); transferMoney(accounts[fromAcct], accounts[toAcct], amount); } } } for (int i = 0; i < NUM_THREADS; i++) new TransferThread().start(); } }

1. Ironiquement, la dtention de verrous pendant des instants brefs, comme on est suppos le faire pour rduire la comptition pour ces verrous, augmente la probabilit que les tests ne dcouvrent pas les risques potentiels dinterblocages. 2. Pour rester simple, DemonstrateDeadlock ne tient pas compte du problme des balances de comptes ngatives.

Chapitre 10

viter les problmes de vivacit

215

10.1.3 Interblocages entre objets coopratifs Lacquisition de plusieurs verrous nest pas toujours aussi vidente que LeftRightDead lock ou transferMoney() car les deux verrous peuvent ne pas tre pris par la mme mthode. Le Listing 10.5 contient deux classes qui pourraient cooprer au sein dune application de rpartition de taxis. La classe Taxi reprsente un taxi particulier avec un emplacement et une destination, la classe Dispatcher reprsente une otte de taxis.
Listing 10.5 : Interblocage li lordre du verrouillage entre des objets coopratifs. Ne le faites pas.
// Attention : risque dinterblocage ! class Taxi { @GuardedBy("this") private Point location, destination; private final Dispatcher dispatcher; public Taxi(Dispatcher dispatcher) { this.dispatcher = dispatcher; } public synchronized Point getLocation() { return location; } public synchronized void setLocation(Point location) { this.location = location; if (location.equals(destination)) dispatcher.notifyAvailable (this); } } class Dispatcher { @GuardedBy("this") private final Set<Taxi> taxis; @GuardedBy("this") private final Set<Taxi> availableTaxis; public Dispatcher() { taxis = new HashSet<Taxi>(); availableTaxis = new HashSet<Taxi>(); } public synchronized void notifyAvailable (Taxi taxi) { availableTaxis .add(taxi); } public synchronized Image getImage() { Image image = new Image(); for (Taxi t : taxis) image.drawMarker(t.getLocation()); return image; } }

Bien quaucune mthode ne prenne explicitement deux verrous, ceux qui appellent set Location() et getImage() peuvent trs bien le faire. Un thread appelant setLocation() en rponse une mise jour dun rcepteur GPS commence par modier lemplacement du taxi puis vrie sil a atteint sa destination. Si cest le cas, il informe le rpartiteur quil attend une nouvelle destination. Comme setLocation() et notifyAvailable() sont deux mthodes synchronized, le thread qui appelle setLocation() prend le verrou

216

Vivacit, performances et tests

Partie III

Taxi puis le verrou Dispatcher. De mme, un thread appelant getImage() prend le verrou Dispatcher puis chaque verrou Taxi (un la fois). Tout comme dans LeftRightDeadlock, deux verrous sont donc pris par deux threads dans un ordre diffrent, ce qui cre un risque dinterblocage.

Il tait assez simple de dtecter la possibilit dun interblocage dans LeftRightDeadlock ou transferMoney() en examinant les mthodes qui prenaient deux verrous. Dans Taxi et Dispatcher, en revanche, cest un peu plus difcile ; le signal rouge est quune mthode trangre est appele pendant que lon dtient un verrou, ce qui risque de poser un problme de vivacit. En effet, cette mthode trangre peut prendre dautres verrous (et donc risquer un interblocage) ou se bloquer pendant un temps inhabituellement long et ger les autres threads qui ont besoin du verrou que lon dtient. 10.1.4 Appels ouverts
Taxi et Dispatcher ne savent videmment pas quils sont les deux moitis dun interblocage possible. Ils ne devraient pas le savoir, dailleurs : un appel de mthode est une barrire dabstraction conue pour masquer les dtails de ce qui se passe de lautre ct. Cependant, comme on ne sait pas ce qui se passe de lautre ct de lappel, linvocation dune mthode trangre quand on dtient un verrou est difcile analyser et donc risqu.

Lappel dune mthode lorsquon ne dtient pas de verrou est un appel ouvert [CPJ 2.4.1.3] et les classes qui utilisent ce type dappel se comportent et se combinent mieux que celles qui font des appels en dtenant des verrous. Lutilisation dappels ouverts pour viter les interblocages est analogue lutilisation de lencapsulation pour la thread safety : bien que lon puisse construire un programme thread-safe sans aucune encapsulation, il est bien plus simple danalyser la thread safety dun programme qui lutilise. De mme, lanalyse de la vivacit dun programme qui utilise uniquement des appels ouverts est bien plus facile. Se limiter des appels ouverts permet de reprer beaucoup plus facilement les endroits du code qui prennent plusieurs verrous et donc de garantir que ces verrous sont pris dans le bon ordre1. Les classes Taxi et Dispatcher du Listing 10.5 peuvent aisment tre modies pour utiliser des appels ouverts et ainsi liminer le risque dinterblocage. Pour cela, il faut rduire les blocs synchronized aux seules oprations portant sur ltat partag, comme on le fait dans le Listing 10.6. Trs souvent, les problmes comme ceux du Listing 10.5 proviennent de lutilisation de mthodes synchronized au lieu de blocs synchronized plus petits, souvent parce que cest plus simple crire et non parce que la mthode entire doit tre protge par un verrou (en outre, la rduction dun bloc synchronized permet galement damliorer ladaptabilit, comme nous le verrons dans la section 11.4.1).
1. La ncessit dutiliser des appels ouverts et dordonner soigneusement les verrous est le reet du dsordre fondamental consistant composer des objets synchroniss au lieu de synchroniser des objets composs.

Chapitre 10

viter les problmes de vivacit

217

Efforcez-vous dutiliser des appels ouverts partout dans vos programmes, car ils seront bien plus simples analyser pour rechercher les ventuels risques dinterblocages que ceux qui appellent des mthodes trangres en dtenant un verrou.

Listing 10.6 : Utilisation dappels ouverts pour viter linterblocage entre des objets coopratifs.
@ThreadSafe class Taxi { @GuardedBy("this") private Point location, destination; private final Dispatcher dispatcher; ... public synchronized Point getLocation() { return location; } public void setLocation(Point location) { boolean reachedDestination ; synchronized (this) { this.location = location; reachedDestination = location.equals(destination); } if (reachedDestination ) dispatcher.notifyAvailable(this); } } @ThreadSafe class Dispatcher { @GuardedBy("this") private final Set<Taxi> taxis; @GuardedBy("this") private final Set<Taxi> availableTaxis; ... public synchronized void notifyAvailable(Taxi taxi) { availableTaxis .add(taxi); } public Image getImage() { Set<Taxi> copy; synchronized (this) { copy = new HashSet<Taxi>(taxis); } Image image = new Image(); for (Taxi t : copy) image.drawMarker(t.getLocation()); return image; } }

Restructurer un bloc synchronized pour permettre des appels ouverts peut parfois avoir des consquences nfastes car cela transforme une opration qui tait atomique en opration non atomique. Dans la plupart des cas, la perte de latomicit est tout fait acceptable ; il ny a aucune raison que la mise jour de lemplacement dun taxi et le fait de prvenir le rpartiteur quil est prt pour une nouvelle destination soit une opration atomique. Dans dautres cas, la perte datomicit est notable mais la modication reste acceptable ; dans la version sujette aux interblocages, getImage() produit un instantan

218

Vivacit, performances et tests

Partie III

complet des emplacements de la otte cet instant, alors que, dans la version modie, elle rcupre les emplacements de chaque taxi des instants lgrement diffrents. Dans certains cas, en revanche, la perte datomicit pose problme et il faut alors utiliser une autre technique. Lune consiste structurer un objet concurrent pour quun seul thread puisse excuter le morceau de code qui suit lappel ouvert. Lorsque lon arrte un service, par exemple, on peut vouloir attendre que les oprations en cours se terminent, puis librer les ressources utilises par le service. Dtenir le verrou du service pendant que lon attend que les oprations se terminent est intrinsquement une source dinterblocage, alors que relcher ce verrou avant larrt du service peut permettre dautres threads de lancer de nouvelles oprations. La solution consiste alors conserver le verrou sufsamment longtemps pour mettre ltat du service "en cours darrt", an que les autres threads qui veulent lancer de nouvelles oprations y compris larrt du service sachent que le service nest pas disponible et nessaient donc pas. On peut alors attendre la n de larrt en sachant que seul le thread darrt a accs ltat du service aprs la n de lappel ouvert. Au lieu dutiliser un verrouillage pour maintenir les autres threads en dehors dune section critique, cette technique repose donc sur la construction de protocoles qui empchent les autres threads de tenter dy entrer. 10.1.5 Interblocages lis aux ressources Tout comme les threads peuvent se bloquer mutuellement lorsque chacun deux attend un verrou que lautre dtient et ne relchera pas, ils peuvent galement sinterbloquer lorsquils attendent des ressources. Les pools de ressources sont gnralement implments laide de smaphores (voir la section 5.5.3) pour faciliter le blocage lorsque le pool est vide. Supposons que lon dispose de deux ressources en pool pour des connexions des bases de donnes diffrentes : si une tche a besoin de connexions vers les deux bases de donnes et que les deux ressources ne sont pas toujours demandes dans le mme ordre, le thread A pourrait dtenir une connexion vers la base B1 tandis quil attendrait une connexion vers la base B2 ; le thread B, quant lui, pourrait possder une connexion vers B2 et attendre une connexion vers B1 (plus les pools sont grands, plus ce risque est rduit ; si chaque pool a N connexions, un interblocage ncessitera N ensembles de threads en attente cyclique et un timing vraiment trs malheureux). Linterblocage par famine de thread est une autre forme dinterblocage li aux ressources. Nous avons vu dans la section 8.1.1 un exemple de ce problme dans lequel une tche qui soumet une tche et attend son rsultat sexcute dans un Executor monothread. En ce cas, la premire tche attendra indniment, ce qui la bloquera, ainsi que toutes celles qui attendent de sexcuter dans cet Executor. Les tches qui attendent les rsultats dautres tches sont les principales sources dinterblocage par famine de threads ; les pools borns et les tches interdpendantes ne vont pas bien ensemble.

Chapitre 10

viter les problmes de vivacit

219

10.2

viter et diagnostiquer les interblocages

Un programme qui ne prend jamais plus dun verrou la fois ne risque pas dinterblocage li lordre du verrouillage. Ce nest, videmment, pas toujours possible mais, si lon peut sen contenter, cela demande beaucoup moins de travail. Si lon doit prendre plusieurs verrous, leur ordre doit faire partie de la conception : on doit essayer de minimiser le nombre dinteractions potentielles entre eux et respecter et dcrire un protocole pour ordonner les verrous susceptibles dtre pris ensemble. Dans les programmes qui utilisent un verrouillage n, on utilise une stratgie en deux parties : on identie dabord les endroits o plusieurs verrous pourraient tre pris (en faisant en sorte quils soient les moins nombreux possible), puis on effectue une analyse globale de toutes ces instances pour sassurer que lordre de verrouillage est cohrent dans lensemble du programme. Lutilisation dappels ouverts chaque fois que cela est possible permet de simplier cette analyse de faon non ngligeable. Avec des appels non ouverts, il est assez facile de trouver les endroits o plusieurs verrous sont pris, soit en relisant le code soit par une analyse automatique du pseudo-code ou du code source. 10.2.1 Tentatives de verrouillage avec expiration Une autre technique pour dtecter les interblocages et en repartir consiste utiliser la fonctionnalit tryLock() temporise des classes Lock explicites (voir Chapitre 13) au lieu du verrouillage interne. Alors que les verrous internes attendent indniment sils ne peuvent pas prendre le verrou, les verrous explicites permettent de prciser un dlai dexpiration aprs lequel tryLock() renverra un chec. En utilisant un dlai peine suprieur celui dans lequel on sattend prendre le verrou, on peut reprendre le contrle en cas dinattendu (le Listing 13.3 montre une autre implmentation de transferMoney() utilisant tryLock() avec de nouvelles tentatives pour augmenter la probabilit dviter les interblocages). On ne peut pas toujours savoir pourquoi une tentative de verrouillage avec dlai choue. Il y a peut-tre eu un interblocage ou un thread est entr par erreur dans une boucle innie alors quil dtenait ce verrou ou une activit sexcute plus lentement que prvu. Quoi quil en soit, on a au moins la possibilit denregistrer que la tentative a chou, dinscrire dans un journal des informations utiles sur ce que lon essayait de faire et de recommencer le traitement autrement quen tuant tout le processus. La prise de verrous avec dlais dexpiration peut se rvler efcace mme lorsquelle nest pas utilise partout dans le programme. Si une prise de verrou ne seffectue pas dans le dlai imparti, on peut relcher les verrous, attendre un peu et ressayer : la situation dinterblocage aura peut-tre disparu et le programme pourra ainsi repartir (cette technique ne fonctionne que lorsque lon prend deux verrous ensemble ; si lon prend plusieurs verrous cause dune imbrication dappels de mthodes, on ne peut pas se contenter de relcher le verrou le plus externe, mme si lon sait quon le dtient).

220

Vivacit, performances et tests

Partie III

10.2.2 Analyse des interblocages avec les traces des threads Bien quviter les interblocages soit essentiellement notre problme, la JVM peut nous aider les identier laide des traces de threads. Une trace de thread contient une pile dappels pour chaque thread en cours dexcution, qui ressemble la pile des appels qui accompagne une exception. Cette trace contient galement des informations sur le verrouillage, notamment quels sont les verrous dtenus par chaque thread, dans quel cadre de pile ils ont t acquis et quel est le verrou qui bloque un thread1. Avant de produire une trace de thread, la JVM recherche les cycles dans le graphe des threads en attente an de trouver les interblocages. Si elle en trouve un, elle inclut des informations qui permettent didentier les verrous et les threads concerns, ainsi que lendroit du programme o ont lieu ces prises de verrous. Pour dclencher une trace de thread, il suft denvoyer le signal SIGQUIT (kill 3) au processus de la JVM sur les plates-formes Unix (ou Ctrl+\) ou Ctrl+Break avec Windows. La plupart des environnements intgrs de dveloppement permettent galement de crer une trace de thread. Java 5.0 ne permet pas dobtenir des informations sur les verrous explicites : ils napparatront donc pas du tout dans les traces de thread. Bien que Java 6 ait ajout le support de ces verrous et la dtection des interblocages avec les Lock explicites, les informations sur les emplacements o ils sont acquis sont ncessairement moins prcises quavec les verrous internes. En effet, ces derniers sont associs au cadre de pile dans lequel ils ont t pris alors que les Lock explicites ne sont associs quau thread qui les a pris. Le Listing 10.7 montre des parties dune trace de thread tire dune application J2EE en production. Lerreur qui a provoqu linterblocage implique trois composants une application J2EE, un conteneur J2EE et un pilote JDBC, chacun tant fourni par des diteurs diffrents (les noms ont t modis pour protger le coupable). Tous les trois taient des produits commerciaux qui sont passs par des phases de tests intensifs ; chacun comprenait un bogue inoffensif jusqu ce quils soient mis ensemble et causent une erreur fatale du serveur.
Listing 10.7 : Portion dune trace de thread aprs un interblocage.
Found one Java-level deadlock: ============================= "ApplicationServerThread ": waiting to lock monitor 0x080f0cdc (a MumbleDBConnection ), which is held by "ApplicationServerThread " "ApplicationServerThread ": waiting to lock monitor 0x080f0ed4 (a MumbleDBCallableStatement ), which is held by "ApplicationServerThread "

1. Ces informations sont utiles pour le dbogage, mme sil ny a pas dinterblocage. Le dclenchement priodique des traces de threads permet dobserver le comportement dun programme vis--vis du verrouillage.

Chapitre 10

viter les problmes de vivacit

221

Java stack information for the threads listed above: "ApplicationServerThread ": at MumbleDBConnection .remove_statement - waiting to lock <0x650f7f30> (a MumbleDBConnection ) at MumbleDBStatement .close - locked <0x6024ffb0> (a MumbleDBCallableStatement ) ... "ApplicationServerThread ": at MumbleDBCallableStatement .sendBatch - waiting to lock <0x6024ffb0> (a MumbleDBCallableStatement ) at MumbleDBConnection .commit - locked <0x650f7f30> (a MumbleDBConnection ) ...

Nous navons montr que la partie de la trace permettant didentier linterblocage. La JVM nous a beaucoup aids en le diagnostiquant elle nous montre les verrous qui ont caus le problme, les threads impliqus et nous indique si dautres threads ont t indirectement touchs. Lun des threads dtient le verrou sur lobjet MumbleDBConnection et attend den prendre un autre sur lobjet MumbleDBCallableStatement ; lautre dtient le verrou sur le MumbleDBCallableStatement et attend celui sur MumbleDBConnection. Le pilote JDBC utilis ici souffre clairement dun bogue li lordre du verrouillage : diffrentes chanes dappels passant par ce pilote acquirent plusieurs verrous dans des ordres diffrents. Mais ce problme ne se serait pas manifest sil ny avait pas un autre bogue : les diffrents threads tentent dutiliser le mme objet Connection JDBC en mme temps. Ce nest pas comme cela que lapplication tait cense fonctionner les dveloppeurs ont t surpris de voir cet objet Connection utilis simultanment par deux threads. Rien dans la spcication de JDBC nexige que Connection soit threadsafe et il est assez courant de conner son utilisation dans un seul thread, comme ctait prvu ici. Cet diteur a tent de fournir un pilote JDBC thread-safe, comme on peut le constater par la synchronisation sur les diffrents objets JDBC utilise dans le code du pilote. Malheureusement, lditeur nayant pas pris en compte lordre du verrouillage, le pilote est expos aux interblocages et cest son interaction avec le partage incorrect de Connection par lapplication qui a provoqu le problme. Comme aucun des deux bogues ntait fatal pris sparment, tous les deux sont passs travers les mailles du let des tests.

10.3

Autres problmes de vivacit

Bien que les interblocages soient les problmes de vivacit les plus frquents, il en existe dautres que vous pouvez rencontrer dans les programmes concurrents : la famine, les signaux manqus et les livelocks (les signaux manqus seront prsents dans la section 14.2.3).

222

Vivacit, performances et tests

Partie III

10.3.1 Famine La famine intervient lorsquun thread se voit constamment refuser laccs des ressources dont il a besoin pour continuer son traitement ; la famine la plus frquente est celle qui concerne les cycles processeurs. Dans les applications Java, la famine peut tre due une mauvaise utilisation des priorits des threads. Elle peut galement tre cause par lexcution de structures de contrles innies (des boucles sans n ou des attentes de ressources qui ne se terminent jamais) pendant la dtention dun verrou puisque les autres threads en attente de ce verrou ne pourront jamais le prendre. Les priorits dnies dans lAPI Thread sont simplement des indications pour lordonnancement. LAPI dnit dix niveaux de priorits que la JVM, si elle le souhaite, peut faire correspondre aux priorits dordonnancement du systme dexploitation. Cette mise en correspondance tant spcique chaque plate-forme, deux priorits Java peuvent donc trs bien correspondre la mme priorit du SE sur un systme et des priorits diffrentes sur un autre. Certains systmes dexploitation ont, en effet, moins de dix niveaux de priorit, auquel cas plusieurs priorits Java correspondent la mme priorit du SE. Les ordonnanceurs des systmes dexploitation sappliquent fournir un ordonnancement bien plus volu que celui qui est requis par la spcication du langage Java. Dans la plupart des applications Java, tous les threads ont la mme priorit, Thread.NORM _PRIORITY. Le mcanisme de priorit des threads est un instrument tranchant et il nest pas toujours vident de savoir quels seront les effets dun changement de priorit ; augmenter celle dun thread peut ne rien donner ou faire en sorte quun seul thread soit plani de prfrence lautre, do une famine de ce dernier. Il est gnralement conseill de rsister la tentation de jouer avec les priorits des threads car, ds que lon commence les modier, le comportement dune application dpend de la plate-forme et lon prend le risque dune famine. La prsence de Thread .sleep ou Thread.yield dans des endroits inhabituels est souvent un indice quun programme essaie de se sortir dune manipulation des priorits et dautres problmes de ractivit en tentant de donner plus de temps aux threads moins prioritaires 1.
vitez dutiliser les priorits des threads car cela augmente la dpendance vis--vis de la plate-forme et peut poser des problmes de vivacit. La plupart des applications concurrentes peuvent se contenter dutiliser la priorit par dfaut pour tous les threads.

1. Les smantiques de Thread.yield (et Thread.sleep(0)) ne sont pas dnies [JLS 17.9] ; la JVM est libre de les implmenter comme des oprations nayant aucun effet ou de les traiter comme des indices dordonnancement. Ces mthodes nont notamment pas ncessairement la mme smantique que celle de sleep(0) sur les systmes Unix qui place le thread courant la n de la le dexcution pour cette priorit et donne le contrle aux autres threads de mme priorit bien que certaines JVM implmentent yield de cette faon.

Chapitre 10

viter les problmes de vivacit

223

10.3.2 Faible ractivit Si lon recule dun pas par rapport la famine, on obtient la faible ractivit, qui est assez frquente dans les applications graphiques qui utilisent des threads en arrire-plan. Au Chapitre 9, nous avons dvelopp un framework pour dcharger les tches longues dans des threads en arrire-plan an de ne pas ger linterface utilisateur. Les tches gourmandes en processeur peuvent malgr tout affecter la ractivit car elles sont en comptition avec le thread des vnements pour laccs au CPU. Cest lun des cas o la modication des priorits des threads peut avoir un intrt : lorsque des calculs en arrire-plan utilisent de faon intensive le processeur. Si le travail effectu par les autres threads sont vraiment des tches en arrire-plan, baisser leur priorit peut rendre les tches de premier plan plus ractives. La faible ractivit peut galement tre due une mauvaise gestion du verrouillage. Si un thread dtient un verrou pendant longtemps (pendant quil parcourt une grosse collection en effectuant un traitement non ngligeable sur chaque lment, par exemple), les autres threads ayant besoin daccder cette collection devront attendre trs longtemps. 10.3.3 Livelock Les livelock sont une forme de perte de vivacit dans laquelle un thread non bloqu ne peut quand mme pas continuer son traitement car il continue de retenter une opration qui chouera toujours. Ils interviennent souvent dans les applications de messagerie transactionnelle, o linfrastructure de messagerie annule une transaction lorsquun message ne peut pas tre trait correctement, pour le replacer en tte de la le. Si un bogue du gestionnaire de message provoque son chec, la transaction sera annule chaque fois que le message est sorti de la le et pass au gestionnaire bogu. Comme le message est replac en tte de le, le gestionnaire sera sans cesse appel et produira le mme rsultat (ce type de situation est parfois appel problme du message empoisonn). Le thread de gestion du message nest pas bloqu mais ne progressera plus non plus. Cette forme de livelock provient souvent dun code de rcupration derreur trop zl qui confond une erreur non rcuprable avec une erreur rcuprable. Les livelock peuvent galement intervenir lorsque plusieurs threads qui cooprent changent leur tat en rponse aux autres de sorte quaucun thread ne peut plus progresser. Cela ressemble ce qui se passe lorsque deux personnes trop polies marchent lune vers lautre dans un couloir : chacune fait un pas de ct pour laisser passer lautre et toutes les deux se retrouvent nouveau en face lune de lautre. Elles font alors un autre cart et peuvent ainsi recommencer ternellement La solution ce type de livelock consiste introduire un peu de hasard dans le mcanisme de ressai. Si deux stations dun rseau Ethernet essaient par exemple denvoyer en mme temps un paquet sur le support partag, il y aura une collision. Les stations dtectent cette collision et essaient de rmettre leurs paquets : si chacune rmet exactement une seconde plus tard, la collision recommencera sans n et le paquet ne partira

224

Vivacit, performances et tests

Partie III

jamais, mme sil y a sufsamment de bande passante. Pour viter ce problme, on fait en sorte que chacune attende pendant un certain moment comprenant une partie alatoire (le protocole Ethernet inclut galement une attente exponentielle aprs les collisions rptes, ce qui rduit la fois la congestion et le risque dun nouvel chec avec les diffrentes stations impliques). Selon le mme principe, une attente alatoire peut galement permettre dviter les livelocks dans les applications concurrentes.

Rsum
Les pertes de vivacit sont un srieux problme car il ny a aucun moyen de sen remettre, part terminer lapplication. La forme la plus frquente est linterblocage li lordre du verrouillage et il ne peut tre rsolu que lors de la conception : il faut sassurer que les threads qui prennent plusieurs verrous le feront toujours dans le mme ordre. Le meilleur moyen de le garantir consiste utiliser des appels ouverts tout au long du programme car cela rduit beaucoup le nombre des emplacements o plusieurs verrous sont dtenus simultanment et rend plus facile leur reprage.

11
Performances et adaptabilit
Lune des principales raisons dutiliser les threads est lamlioration des performances 1. Grce leur emploi, on amliorera lutilisation des ressources en permettant aux applications dexploiter plus facilement la capacit de traitement disponible et la ractivit en les autorisant lancer immdiatement de nouvelles tches pendant que dautres sexcutent encore. Ce chapitre explore les techniques permettant danalyser, de surveiller et damliorer les performances des programmes concurrents. Malheureusement, beaucoup de techniques doptimisation des performances augmentent galement la complexit et, par consquent, les risques de problmes de scurit vis--vis des threads et les risques de perte de vivacit. Pire encore, certaines techniques destines amliorer les performances sont, en fait, contre-productives ou changent un type de problme contre un autre. Bien quil soit souvent souhaitable doptimiser les performances, la scurit vis--vis des threads est toujours prpondrante. Il faut dabord que le programme soit correct avant de lacclrer et uniquement sil y en a besoin. Lorsque lon conoit une application concurrente, extraire la dernire goutte de performance est souvent le dernier de nos soucis.

11.1

Penser aux performances

Amliorer les performances consiste en faire plus avec moins de ressources. La signification de "ressource" peut varier ; pour une activit donne, certaines ressources spciques sont gnralement trop peu nombreuses, que ce soient les cycles processeurs, la mmoire, la bande passante du rseau, celle des E/S, les requtes aux bases de donnes, lespace disque ou nimporte quelle autre quantit de ressource. Lorsque les performances
1. Certains pourraient rtorquer que cest la seule que nous supportons avec la complexit que les threads introduisent.

226

Vivacit, performances et tests

Partie III

dune activit sont limites par la disponibilit dune ressource particulire, on dit quelle est lie cette ressource : on parle alors dactivit lie au processeur, une base de donnes, etc. Bien que le but puisse tre damliorer les performances globales, lutilisation de plusieurs threads a toujours un cot par rapport lapproche monothread. Ce prix payer inclut le surcot d la coordination entre les threads (verrouillage, signaux et synchronisation de la mmoire), aux changements de contexte supplmentaires, la cration et la suppression des threads et, enn, leur planication. Lorsque les threads sont employs correctement, ces surcots seffacent devant laccroissement du dbit des donnes, de la ractivit ou de la capacit de traitement. En revanche, une application concurrente mal conue peut avoir des performances bien infrieures celles dun programme squentiel1. En utilisant la concurrence pour accrotre les performances, on essaie de raliser deux buts : utiliser plus efcacement les ressources de traitement disponibles et permettre au programme dexploiter les ressources de traitement supplmentaires lorsquelles sont mises disposition. Du point de vue de la surveillance des performances, ceci signie que nous tentons doccuper le plus possible le processeur (ce qui ne signie pas, bien sr, quil faille consommer des cycles avec des calculs sans intrt ; nous voulons loccuper avec du travail utile). Si le programme est li au processeur, nous devrions donc pouvoir augmenter sa capacit de traitement en ajoutant plus de processeurs, mais, sil ne peut mme pas occuper les processeurs disponibles, en ajouter dautres ne lui servira rien. Les threads offrent un moyen de "faire chauffer" les CPU en dcomposant lapplication de sorte quun processeur disponible ait toujours du travail faire. 11.1.1 Performances et adaptabilit Les performances dune application peuvent se mesurer de diffrentes faons : en tant que temps de service, de latence, de dbit, defcacit, dadaptabilit ou de capacit. Certaines (le temps de service et la latence) mesurent la "rapidit" laquelle une unit de travail peut tre traite ou prise en compte : dautres (la capacit et le dbit) valuent la "quantit" de travail qui peut tre effectue avec un nombre donn de ressources de calcul.
Ladaptabilit dcrit la facult damliorer le dbit ou la capacit lorsque lon ajoute de nouvelles ressources (processeurs, mmoire, disques ou bande passante).

1. Un collgue ma relat cette anecdote amusante : il avait t charg du test dune application chre et complexe qui effectuait ses traitements via un pool de threads congurable. Lorsque le systme a t termin, les tests ont montr que le nombre optimal de threads pour le pool tait 1. Cela aurait d tre vident ds le dbut puisque le systme cible tait monoprocesseur et que lapplication tait presque entirement lie au processeur.

Chapitre 11

Performances et adaptabilit

227

La conception et la conguration dapplications concurrentes adaptables peuvent tre trs diffrentes dune optimisation classique des performances. En effet, amliorer les performances consiste gnralement faire en sorte deffectuer le mme traitement avec moins deffort, en rutilisant par exemple des rsultats mis en cache ou en remplaant un algorithme en O(n2)1 par un autre en O(n log n). Pour ladaptabilit, en revanche, on recherche des moyens de parallliser le problme an de tirer parti des ressources supplmentaires : le but consiste donc en faire plus avec plus de ressources. Ces deux aspects des performances rapidit et quantit sont totalement disjoints et, parfois, opposs lun lautre. Pour obtenir une meilleure adaptabilit ou une utilisation optimale du matriel, on nit souvent par augmenter le volume de travail faire pour traiter chaque tche, en divisant par exemple les tches en plusieurs sous-tches individuelles "en pipeline". Ironiquement, la plupart des astuces qui amliorent les performances des programmes monothreads sont mauvaises pour ladaptabilit (la section 11.4.4 prsentera un exemple). Le modle trois-tier classique dans lequel la couche prsentation, la couche mtier et la couche accs aux donnes sont spares et peuvent tre prises en charge par des systmes diffrents illustre bien comment des amliorations en termes dadaptabilit se font souvent aux dpens des performances. Une application monolithique dans laquelle ces trois couches sont mlanges aura presque toujours de meilleures performances quune autre multi-tier, bien conue, distribue sur plusieurs systmes. Comment pourrait-il en tre autrement ? Lapplication monolithique ne souffre pas de la latence du rseau inhrente au passage des tches entre les couches et na pas payer le prix de la sparation dun traitement en plusieurs couches abstraites (notamment les surcots dus la mise en le dattente, la coordination et la copie des donnes). Cependant, il se posera un srieux problme lorsque ce systme monolithique aura atteint sa capacit de traitement car il peut tre extrmement difcile de laugmenter de faon signicative. On accepte donc gnralement de payer le prix dun plus long temps de service ou dutiliser plus de ressources par unit de travail an que notre application puisse sadapter une charge plus lourde si on lui ajoute plus de ressources. Des diffrents aspects des performances, ceux concernant la quantit ladaptabilit, le dbit et la capacit sont gnralement plus importants pour les applications serveur que ceux lis la rapidit (pour les applications interactives, la latence est le critre le plus important car les utilisateurs nont pas besoin dindicateurs de progression ou de se demander ce qui se passe). Dans ce chapitre, nous nous intresserons essentiellement ladaptabilit plutt quaux performances pures dans un contexte monothread.

1. Une complexit en O(n2) signie que les performances de lalgorithme sont proportionnelles au carr du nombre des lments.

228

Vivacit, performances et tests

Partie III

11.1.2 Compromis sur lvaluation des performances Quasiment toutes les dcisions techniques impliquent un compromis. Utiliser de lacier plus pais pour un pont, par exemple, peut augmenter sa capacit et sa scurit, mais galement son cot de construction. Si les dcisions informatiques nimpliquent gnralement pas de compromis entre le prix et les risques de vies humaines, on dispose galement de moins dinformations permettant de faire les bons choix. Lalgorithme de "tri rapide", par exemple, est trs efcace sur les grands volumes de donnes alors que le "tri bulles", moins sophistiqu, est en fait plus performant que lui sur les petits ensembles. Si lon vous demande dimplmenter une fonction de tri efcace, vous devez donc avoir une ide du volume de donnes que vous devrez traiter et savoir si vous voulez optimiser le cas moyen, le pire des cas ou la prdictabilit. Malheureusement, ces informations ne font souvent pas partie du cahier des charges remis lauteur dune fonction de tri, et cest lune des raisons pour lesquelles la plupart des optimisations sont prmatures : elles sont souvent entreprises avant davoir une liste claire des conditions remplir.
vitez les optimisations prmatures. Commencez par faire correctement les choses, puis acclrez-les si cela ne va pas assez vite.

Lorsque lon prend des dcisions techniques, on doit parfois troquer une forme de cot contre une autre (le temps de service contre la consommation mmoire, par exemple) ; parfois, on change le cot contre la scurit. Cette scurit ne signie pas ncessairement que des vies humaines sont en jeu comme dans lexemple du pont. De nombreuses optimisations des performances se font au dtriment de la lisibilit ou de la facilit de maintenance plus le code est "astucieux" ou non vident, plus il est difcile comprendre et maintenir. Les optimisations supposent parfois de remettre en cause les bons principes de conception oriente objet, de casser lencapsulation, par exemple ; elles augmentent quelquefois les risques derreur car les algorithmes plus rapides sont gnralement plus compliqus (si vous ne pouvez pas identier les cots ou les risques, vous ny avez srement pas assez rchi pour continuer). La plupart des dcisions lies aux performances impliquent plusieurs variables et dpendent fortement de la situation. Avant de dcider quune approche est "plus rapide" quune autre, vous devez vous poser les questions suivantes :
m m

Que signie "plus rapide" ? Sous quelles conditions cette approche sera vraiment plus rapide ? Sous une charge lgre ou lourde ? Avec de petits ou de gros volumes de donnes ? Pouvez-vous tayer votre rponse par des mesures ? Combien de fois ces conditions risquent-elles davoir lieu dans votre situation ? Pouvez-vous tayer votre rponse par des mesures ?

Chapitre 11

Performances et adaptabilit

229

Est-ce que le code sera utilis dans dautres situations, o les conditions peuvent tre diffrentes ? Quels cots cachs, comme laugmentation des risques de dveloppement ou de maintenance, changez-vous contre cette amlioration des performances ? Le jeu en vaut-il la chandelle ?

Toutes ces considrations sappliquent nimporte quelle dcision technique lie aux performances, mais il sagit ici dun livre sur la programmation concurrente : pourquoi recommandons-nous une telle prudence vis--vis de loptimisation ? Parce que la qute de performance est srement lunique source majeure des bogues de concurrence. La croyance que la synchronisation tait "trop lente" a produit des idiomes apparemment astucieux, mais dangereux, pour la rduire (comme le verrouillage contrl deux fois, que nous prsenterons dans la section 16.2.4) et elle est souvent cite comme excuse pour ne pas respecter les rgles concernant la synchronisation. Les bogues de concurrence faisant partie des plus difciles reprer et supprimer, tout ce qui risque de les introduire doit tre entrepris avec beaucoup de prcautions. Pire encore, lorsque vous changez la scurit contre les performances, vous pouvez nobtenir aucune des deux. Lorsquil sagit de concurrence, lintuition de nombreux dveloppeurs concernant lemplacement dun problme de performances ou lapproche qui sera plus rapide ou plus adaptable est souvent incorrecte. Il est donc impratif que toute tentative damlioration des performances soit accompagne dexigences concrtes (an de savoir quand tenter damliorer et quand arrter) et dun programme de mesures utilisant une conguration raliste et un prol de charge. Mesurez aprs la modication pour vrier que vous avez obtenu lamlioration escompte. Les risques de scurit et de maintenance associs de nombreuses optimisations sont sufsamment graves vous ne souhaitez pas payer ce prix si vous nen navez pas besoin et vous ne voudrez vraiment pas le payer si vous nobtenez aucun bnce.
Mesurez, ne supposez pas.

Bien quil existe des outils de prolage sophistiqus permettant de mesurer les performances et de reprer les goulets dtranglement qui les pnalisent, vous navez pas besoin de dpenser beaucoup dargent pour savoir ce que fait votre programme. Lapplication gratuite perfbar, par exemple, peut vous donner une bonne ide de loccupation du processeur : votre but tant gnralement de loccuper au maximum, cest un trs bon moyen dvaluer le besoin dajuster les performances et leffet de cet ajustement.

230

Vivacit, performances et tests

Partie III

11.2

La loi dAmdahl

Certains problmes peuvent se rsoudre plus rapidement lorsquon dispose de plus de ressources plus il y a douvriers pour faire les rcoltes, plus elles sont termines rapidement. Dautres tches sont fondamentalement squentielles ce nest pas un nombre plus grand douvriers qui feront pousser plus vite la rcolte. Si lune des raisons principales dutiliser les threads est dexploiter la puissance de plusieurs processeurs, il faut galement sassurer que le problme peut tre paralllis et que le programme exploite effectivement ce paralllisme. La plupart des programmes concurrents ont beaucoup de points communs avec lagriculture, qui est un mlange de parties paralllisables et squentielles. La loi dAmdahl prcise lamlioration thorique de la vitesse dun programme par rapport au nombre de ressources supplmentaires daprs la proportion de ses composantes paralllisables et squentielles. Si F est la fraction de calcul qui doit tre excute squentiellement, cette loi indique que, sur une machine N processeurs, on peut obtenir un gain de vitesse dau plus :
gain de vitesse F+ 1 (1 - F )

Lorsque N tend vers linni, le gain de vitesse maximal converge vers 1/F, ce qui signie quun programme dont cinquante pour-cent des calculs doivent seffectuer en squence ne peut tre acclr que par un facteur de deux, quel que soit le nombre de processeurs disponibles ; un programme o dix pour-cent doit tre excut en srie peut tre acclr par, au plus, un facteur de dix. La loi dAmdahl quantie galement le cot de lefcacit de la srialisation. Avec dix processeurs, un programme squentiel 10 % peut esprer au plus une acclration de 5,3 ( 53 % dutilisation) ; avec cent processeurs, il peut obtenir un gain maximal de vitesse de 9,2 ( 9 % dutilisation). Il faut beaucoup de processeurs mal utiliss pour ne jamais obtenir ce facteur de 10. La Figure 11.1 montre lutilisation maximale des processeurs pour diffrents degrs dexcution squentielle et diffrents nombres de processeurs (lutilisation est dnie comme le gain de vitesse divis par le nombre de processeurs). Il apparat clairement que, mesure que le nombre de CPU augmente, mme un petit pourcentage dexcution squentielle limite lamlioration possible du dbit lorsquon ajoute des ressources supplmentaires. Le Chapitre 6 a montr comme identier les frontires logiques qui permettent de dcomposer les applications en tches. Cependant, il faut galement identier les sources de srialisation dans les tches an de prvoir le gain quil est possible dobtenir en excutant une application sur un systme multiprocesseurs.

Chapitre 11

Performances et adaptabilit

231

Figure 11.1
Utilisation maximale selon la loi dAmdahl pour diffrents pourcentages de srialisation.
Utilisation 1.0 %

0.8 %

0.6 %

0.4 %

0.2 % 0.0 % 0 20 40 60 80 100 Nombre de processeurs

Imaginons une application o N threads excutent la mthode doWork() du Listing 11.1, rcuprent les tches dans une le dattente partage et les traitent en supposant que les tches ne dpendent pas des rsultats ou des effets de bords des autres tches. Si lon ne tient pas compte pour le moment de la faon dont les tches sont places dans la le, est-ce que cette application sadaptera bien lorsque lon ajoutera des processeurs ? Au premier abord, il peut sembler quelle est totalement paralllisable : les tches ne sattendent pas les unes et les autres et, plus il y aura de processeurs, plus lapplication pourra traiter de tches en parallle. Cependant, lapplication comprend galement une composante squentielle la rcupration dune tche dans la le dattente. Cette dernire est partage par tous les threads et ncessite donc un peu de synchronisation pour maintenir son intgrit face aux accs concurrents. Si lon utilise un verrou pour protger ltat de cette le, un thread voulant prendre une tche dans la le forcera les autres qui veulent prendre la leur attendre et cest l que le traitement des tches est srialis.
Listing 11.1 : Accs squentiel une file dattente.
public class WorkerThread extends Thread { private final BlockingQueue <Runnable> queue; public WorkerThread(BlockingQueue <Runnable> queue) { this.queue = queue; } public void run() { while (true) { try { Runnable task = queue.take(); task.run(); } catch (InterruptedException e) { break; /* Permet au thread de se terminer */ } } } }

232

Vivacit, performances et tests

Partie III

Le temps de traitement dune seule tche inclut non seulement le temps dexcution de la tche Runnable, mais galement celui ncessaire lextraction de la tche partir de la le dattente partage. Si cette le est une LinkedBlockingQueue, lopration dextraction peut tre moins bloquante quavec une LinkedList synchronise car LinkedBlockingQueue utilise un algorithme mieux adapt ; cependant, laccs toute structure de donnes partage introduit fondamentalement un lment de srialisation dans un programme. Cet exemple ignore galement une autre source de srialisation : la gestion du rsultat. Tous les calculs utiles produisent un rsultat ou un effet de bord dans le cas contraire, ils pourraient tre supprims puisquil sagirait de code mort. Runnable ne fournissant aucun traitement explicite du rsultat, ces tches doivent donc avoir un certain effet de bord, en crivant par exemple leur rsultat dans un chier journal ou en le plaant dans une structure de donnes. Les chiers journaux et les conteneurs de rsultats tant gnralement partags par plusieurs threads, ils constituent donc galement une source de srialisation. Si chaque thread grait sa propre structure de rsultat et que toutes ces structures taient fusionnes la n des tches, cette fusion nale serait aussi une source de srialisation.
Toutes les applications concurrentes comprennent des sources de srialisation. Si vous pensez que la vtre nen a pas, regardez mieux.

11.2.1 Exemple : srialisation cache dans les frameworks Pour voir comment la srialisation peut se cacher dans la structure dune application, nous pouvons comparer les dbits mesure que lon ajoute des threads et dduire les diffrences en termes de srialisation daprs les diffrences observes dans ladaptabilit. La Figure 11.2 montre une application simple dans laquelle plusieurs threads rptent la suppression dun lment dune Queue partage et le traitent, un peu comme dans le Listing 11.1. Ltape de traitement nest quun calcul local au thread. Si un thread trouve la le vide, il y ajoute un lot de nouveaux lments pour que les autres aient quelque chose traiter lors de leur prochaine itration. Laccs la le partage implique videmment un peu de srialisation, mais ltape de traitement est entirement paralllisable puisquelle nutilise aucune donne partage. Les courbes de la Figure 11.2 comparent les dbuts des deux implmentations thread-safe de Queue : une LinkedList enveloppe avec synchronizedList() et une Concurrent LinkedQueue. Les tests ont t effectus sur une machine Sparc V880 huit processeurs avec le systme Solaris. Bien que chaque excution reprsente la mme somme de "travail", nous pouvons constater que le simple fait de changer dimplmentation de le peut avoir de lourdes consquences sur ladaptabilit.

Chapitre 11

Performances et adaptabilit

233

Figure 11.2
Comparaison des implmentations de les dattente.
Dbit (normalis)

ConcurrentLinkedQueue

2 synchronized LinkedList

0 1 4 8 Nombre de threads 12 16

Le dbit de ConcurrentLinkedQueue continue en effet daugmenter jusqu ce quil atteigne le nombre de processeurs, puis reste peu prs constant. Le dbit de la Linked List synchronise, en revanche, montre une amlioration jusqu trois threads, puis chute mesure que le cot de la synchronisation augmente. Au moment o elle passe quatre ou cinq threads, la comptition est si lourde que chaque accs au verrou de la le est trs disput et que le dbit est domin par les changements de contexte. La diffrence de dbit provient des degrs de srialisation diffrents entre les deux implmentations. La LinkedList synchronise protge ltat de toute la le avec un seul verrou qui est dtenu pendant la dure de lajout ou de la suppression ; ConcurrentLinkedQueue, en revanche, utilise un algorithme non bloquant sophistiqu (voir la section 15.4.2) qui se sert de rfrences atomiques pour mettre jour les diffrents pointeurs de liens. Dans la premire, toute linsertion ou toute la suppression est srialise ; dans la seconde, seules les modications des diffrents pointeurs sont traites squentiellement. 11.2.2 Application qualitative de la loi dAmdahl La loi dAmdahl quantie le gain de vitesse possible lorsque lon dispose de ressources de traitement supplmentaires condition de pouvoir estimer prcisment la fraction squentielle de lexcution. Bien quil puisse tre difcile dvaluer directement cette srialisation, la loi dAmdahl peut quand mme tre utile sans cette mesure. Nos modles mentaux tant inuencs par notre environnement, nombre dentre nous pensons quun systme multiprocesseurs est dot de deux ou quatre processeurs ou, en cas de gros budget, de plusieurs dizaines puisque cest cette technologie qui nous est propose un peu partout depuis quelques annes. Mais, lorsque les processeurs multicurs se seront rpandus, les systmes auront des centaines, voire des milliers de processeurs 1. Les
1. Mise jour : lheure o ce livre est crit, Sun vend des serveurs dentre de gamme reposant sur le processeur Niagara 8 curs et Azul vend des serveurs haut de gamme (96, 192 et 384 processeurs) exploitant le processeur Vega 24 curs.

234

Vivacit, performances et tests

Partie III

algorithmes qui semblent adaptables sur un systme quatre processeurs peuvent donc avoir des goulets dtranglement pour leur adaptabilit qui nont tout simplement pas encore t rencontrs. Lorsquon value un algorithme, penser ce qui se passera avec des centaines ou des milliers de processeurs permet davoir une ide des limites dadaptabilit qui peuvent apparatre. Les sections 11.4.2 et 11.4.3, par exemple, prsentent deux techniques pour rduire la granularit du verrouillage : la division des verrous (diviser un verrou en deux) et le dcoupage des verrous (diviser un verrou en plusieurs). En les tudiant la lumire de la loi dAmdahl, on saperoit que la division dun verrou en deux ne nous amne pas bien loin dans lexploitation de nombreux processeurs, alors que le dcoupage de verrous semble plus prometteur puisque le nombre de dcoupages peut tre augment en fonction du nombre de processeurs (les optimisations des performances devraient, bien sr, toujours tre considres la lumire des exigences de performances relles ; dans certains cas, diviser un verrou en deux peut donc sufre).

11.3

Cots lis aux threads

Les programmes monothreads nimpliquent pas de surcots dus la planication ou la synchronisation et nutilisent pas de verrous pour prserver la cohrence des structures de donnes. La planication et la coordination entre les threads ont un cot en termes de performances : pour que les threads amliorent la rapidit dun programme, il faut donc que les bnces de la paralllisation soient suprieurs aux cots induits par la concurrence. 11.3.1 Changements de contexte Si le thread principal est le seul thread, il ne sera quasiment jamais dprogramm. Si, en revanche, il y a plus de threads que de processeurs, le systme dexploitation nira par prempter un thread pour permettre un autre dutiliser son tour le processeur. Cela provoque donc un changement de contexte qui ncessite de sauvegarder le contexte du thread courant et de restaurer celui du thread nouvellement plani. Les changements de contexte ne sont pas gratuits car la planication implique une manipulation de structures de donnes partages dans le SE et dans la JVM. Tous les deux utilisant les mmes processeurs que le programme, plus il y a de temps CPU pass dans le code du SE et de la JVM, moins il en reste pour le programme. Cependant, les activits du SE et de la JVM ne sont pas les seuls cots induits par un changement de contexte. Lorsquun thread est plani, les donnes dont il a besoin ne se trouvent srement pas dans le cache local du processeur : le changement de contexte produira donc une rafale derreurs de cache et le thread sexcutera alors un peu plus lentement au dbut. Cest lune des raisons pour lesquelles les planicateurs octroient chaque thread qui sexcute un quantum de temps minimal, mme si de nombreux autres threads sont en attente : cela permet damortir le cot du changement de contexte en le diluant dans un

Chapitre 11

Performances et adaptabilit

235

temps dexcution plus long et non interrompu, ce qui amliore le dbit (aux dpens de la ractivit). Lorsquun thread se bloque parce quil attend de prendre un verrou disput par dautres threads, la JVM le suspend an de pouvoir le dprogrammer. Si des threads se bloquent souvent, ils ne pourront donc pas utiliser tout le quantum de temps qui leur est allou. Un programme qui se bloque plus souvent ( cause dE/S bloquantes, dattentes de verrous trs disputs ou de variables conditions) implique par consquent plus de changements de contexte quun programme li au processeur, ce qui augmente le surcot de la planication et rduit le dbit (les algorithmes non bloquants peuvent permettre de rduire ces changements de contexte ; voir Chapitre 15). Le vritable cot dun changement de contexte varie en fonction des plates-formes, mais une bonne estimation consiste considrer quil est quivalent 5 000-10 000 cycles dhorloge, soit plusieurs microsecondes sur la plupart des processeurs actuels. La commande vmstat des systmes Unix et loutil perfmon de Windows permettent dvaluer le nombre de changements de contexte et le pourcentage de temps pass dans le noyau. Une forte utilisation du noyau (plus de 10 %) indique souvent une forte activit de planication, qui peut tre due des E/S bloquantes ou une lutte pour lobtention de verrous. 11.3.2 Synchronisation de la mmoire Le cot de la synchronisation en termes de performances a plusieurs sources. Les garanties de visibilit offertes par synchronized et volatile peuvent impliquer des instructions spciales appeles barrires mmoires qui peuvent vider ou invalider des caches, vider les tampons physiques dcriture et ger les pipelines dexcution. Les barrires mmoires peuvent galement avoir des consquences indirectes sur les performances car elles inhibent certaines optimisations du compilateur ; elles lempchent de rordonner la plupart des oprations. Lorsque lon veut estimer limpact de la synchronisation sur les performances, il est important de faire la distinction entre synchronisation avec comptition et sans comptition. Le mcanisme synchronized est optimis pour le cas sans comptition (volatile est toujours sans comptition) et, au moment o ce livre est crit, le cot dune synchronisation "rapide" sans comptition varie de 20 250 cycles dhorloge sur la plupart des systmes. Bien quil ne soit certainement pas nul, leffet dune synchronisation sans comptition est donc rarement signicatif dans les performances globales dune application ; son alternative implique de compromettre la scurit et de prendre le risque de devoir rechercher des bogues par la suite, ce qui est une opration trs pnible pour vous ou vos successeurs. Les JVM actuelles peuvent rduire le cot de la synchronisation annexe en optimisant le verrouillage quand il est certain quil sera sans comptition. Si un objet verrou nest

236

Vivacit, performances et tests

Partie III

accessible que par le thread courant, la JVM peut optimiser sa prise puisquil nest pas possible quun autre thread puisse se synchroniser dessus. Lacquisition du verrou dans le Listing 11.2, par exemple, peut donc toujours tre limine par la JVM.
Listing 11.2 : Synchronisation inutile. Ne le faites pas.
synchronized (new Object()) { // faire quelque chose }

Les JVM plus sophistiques peuvent analyser les chappements pour dceler quand une rfrence dobjet local nest jamais publie sur le tas et est donc locale au thread. Dans la mthode getStoogeNames() du Listing 11.3, par exemple, la seule rfrence lobjet List est la variable locale stooges et les variables connes la pile sont automatiquement locales au thread. Une excution nave de getStoogeNames() prendrait et relcherait quatre fois le verrou sur le Vector, un pour chaque appel add() ou toString(). Cependant, un compilateur un peu malin peut traduire ces appels en ligne, constater que stooges et son tat interne ne schapperont jamais et donc liminer les quatre prises du verrou1.
Listing 11.3 : Candidat llision de verrou.
public String getStoogeNames() { List<String> stooges = new Vector<String>(); stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); return stooges.toString(); }

Mme sans analyse des chappements, les compilateurs peuvent galement effectuer un paississement de verrou, cest--dire fusionner les blocs synchroniss adjacents qui utilisent le mme verrou. Dans le cas de getStoogeNames(), une JVM effectuant un paississement de verrou pourrait combiner les trois appels add() et lappel toString() en une prise et un relchement dun seul verrou en se servant dheuristiques sur le cot relatif de la synchronisation par rapport aux instructions dans le bloc synchronized2. Non seulement cela rduit le surcot de la synchronisation, mais cela fournit galement au compilateur un plus gros bloc traiter, ce qui permet dactiver dautres optimisations.

1. Cette optimisation, appele lision de verrou, est effectue par la JVM IBM et est attendue dans Hostpot de Java 7. 2. Un compilateur dynamique astucieux pourrait se rendre compte que cette mthode renvoie toujours la mme chane et recompiler getStoogeNames() aprs sa premire excution pour quelle renvoie simplement la valeur renvoye par son premier appel.

Chapitre 11

Performances et adaptabilit

237

Ne vous faites pas trop de souci sur le cot de la synchronisation sans comptition. Le mcanisme de base est dj assez rapide et les JVM peuvent encore effectuer des optimisations supplmentaires pour rduire ou liminer ce cot. Intressez-vous plutt loptimisation des parties comprenant des verrous avec comptition.

La synchronisation par un seul thread peut galement affecter les performances des autres threads car elle cre du trac sur le bus de la mmoire partage, or ce bus a une bande passante limite et il est partag par tous les processeurs. Si les threads doivent se battre pour la bande passante de la synchronisation, ils en souffriront tous 1. 11.3.3 Blocages La synchronisation sans comptition peut tre entirement gre dans la JVM (Bacon et al., 1998) alors que la synchronisation avec comptition peut demander une activit du systme dexploitation, qui a galement un cot. Lorsque le verrouillage est disput, le ou les threads qui perdent doivent se bloquer. La JVM peut implmenter le blocage par une attente tournante (elle ressaie dobtenir le verrou jusqu ce que cela russisse) ou en suspendant le thread bloqu via le systme dexploitation. La mthode la plus efcace dpend de la relation entre le cot du changement de contexte et le temps quil faut attendre avant que le verrou soit disponible ; lattente tournante est prfrable pour les attentes courtes alors que la suspension est plus adapte aux longues. Certaines JVM choisissent entre les deux en utilisant des informations sur des temps dattente passs, mais la plupart se contentent de suspendre les threads qui attendent un verrou. La suspension dun thread parce quil ne peut pas obtenir un verrou ou parce quil est bloqu sur une attente de condition ou dans une opration dE/S bloquante implique deux changements de contexte supplmentaires, avec toute lactivit du SE et du cache qui leur est associe : le thread bloqu est sorti de lunit centrale avant la n de son quantum de temps et il y est remis plus tard, lorsque le verrou ou une autre ressource devient disponible (le blocage d la comptition pour un verrou a galement un cot pour le thread qui dtient ce verrou : quand il le relche, il doit en plus demander au SE de relancer le thread bloqu).

11.4

Rduction de la comptition pour les verrous

Nous avons vu que la srialisation dtriorait ladaptabilit et que les changements de contexte pnalisaient les performances. Le verrouillage avec comptition souffrant de ces deux problmes, le rduire permet damliorer la fois les performances et ladaptabilit.
1. Cet aspect est parfois utilis comme argument contre lutilisation des algorithmes non bloquants sous une forme ou sous une autre de repli car, en cas de comptition importante, ces algorithmes produisent plus de trac de synchronisation que ceux qui utilisent des verrous. Voir Chapitre 15.

238

Vivacit, performances et tests

Partie III

Laccs aux ressources protges par un verrou exclusif est srialis un seul thread peut y accder la fois. Nous utilisons bien sr les verrous pour de bonnes raisons, comme empcher la corruption des donnes, mais cette scurit a un prix. Une comptition persistante pour un verrou limite ladaptabilit.
La principale menace contre ladaptabilit des applications concurrentes est le verrou exclusif sur une ressource.

La comptition pour un verrou est inuence par deux facteurs : la frquence laquelle ce verrou est demand et le temps pendant lequel il est conserv une fois quil a t pris 1. Si le produit de ces facteurs est sufsamment petit, la plupart des tentatives dacquisition se feront sans comptition et la lutte pour ce verrou ne posera pas de problme signicatif pour ladaptabilit. Si, en revanche, le verrou est sufsamment disput, les threads qui tentent de lacqurir seront bloqus ; dans le pire des cas, les processeurs resteront inactifs bien quil y ait beaucoup de travail faire.
Il y a trois moyens de rduire la comptition pour les threads : rduire la dure de possession des verrous ; rduire la frquence des demandes de verrous ; remplacer les verrous exclusifs par des mcanismes de coordination permettant une plus grande concurrence.

11.4.1 Rduction de la porte des verrous ("entrer, sortir") Un moyen efcace de rduire la probabilit de comptition consiste maintenir les verrous le plus brivement possible, ce qui peut tre ralis en dplaant le code qui ne ncessite pas de verrouillage en dehors des blocs synchronized surtout lorsquil sagit doprations coteuses et potentiellement bloquantes, comme des E/S. Il est assez facile de constater que dtenir trop longtemps un verrou "chaud" peut limiter ladaptabilit ; nous en avons vu un exemple avec la classe SynchronizedFactorizer du Chapitre 2. Si une opration dtient un verrou pendant 2 millisecondes et que chaque opration ait besoin de ce verrou, le dbit ne pourra pas tre suprieur 500 oprations par seconde, quel que soit le nombre de processeurs disponibles. Rduire le temps de

1. Cest un corrolaire de la loi de Little, un rsultat de la thorie des les qui nonce que "le nombre moyen de clients dans un systme stable est gal leur frquence moyenne darrive multiplie par leur temps moyen dans le systme" (Little, 1961).

Chapitre 11

Performances et adaptabilit

239

dtention du verrou 1 milliseconde repousse la limite du dbit 1 000 oprations par seconde1. La classe AttributeStore du Listing 11.4 montre un exemple de dtention dun verrou plus longtemps que ncessaire. La mthode userLocationMatches() recherche lemplacement de lutilisateur dans un Map en utilisant des expressions rgulires. Toute la mthode est synchronise alors que la seule portion du code qui a rellement besoin du verrou est lappel Map.get().
Listing 11.4 : Dtention dun verrou plus longtemps que ncessaire.
@ThreadSafe public class AttributeStore { @GuardedBy("this") private final Map<String, String> attributes = new HashMap<String, String>(); public synchronized boolean userLocationMatches(String name, String regexp) { String key = "users." + name + ".location"; String location = attributes.get(key); if (location == null) return false; else return Pattern.matches(regexp, location); } }

La classe BetterAttributeStore du Listing 11.5 rcrit AttributeStore en rduisant de faon signicative la dure de dtention du verrou. La premire tape consiste construire la cl du Map associe lemplacement de lutilisateur une chane de la forme users.nom.location , ce qui implique dinstancier un objet StringBuilder, de lui ajouter plusieurs chanes et de rcuprer le rsultat sous la forme dun objet String. Puis lexpression rgulire est mise en correspondance avec cette chane. La construction de cette cl et le traitement de lexpression rgulire nutilisant pas ltat partag, ces oprations nont pas besoin dtre excutes pendant la dtention du verrou : Better AttributeStore les regroupe par consquent lextrieur du bloc synchronized an de rduire la dure du verrouillage.
Listing 11.5 : Rduction de la dure du verrouillage.
@ThreadSafe public class BetterAttributeStore { @GuardedBy("this") private final Map<String, String> attributes = new HashMap<String, String>(); public boolean userLocationMatches(String name, String regexp) { String key = "users." + name + ".location"; String location;

1. En ralit, ce calcul sous-estime le cot dune longue dtention des verrous car il ne prend pas en compte le surcot des changements de contexte produits par la comptition accrue pour lobtention du verrou.

240

Vivacit, performances et tests

Partie III

Listing 11.5 : Rduction de la dure du verrouillage. (suite)


synchronized (this) { location = attributes.get(key); } if (location == null) return false; else return Pattern.matches(regexp, location); } }

Rduire la porte du verrou dans userLocationMatches() diminue de faon non ngligeable le nombre dinstructions qui sont excutes pendant que le verrou est pris. Selon la loi dAmdahl, cela supprime un obstacle ladaptabilit car le volume de code srialis est moins important.
AttributeStore nayant quune seule variable dtat, attributes, nous pouvons encore amliorer la technique en dlguant la thread safety (voir la section 4.3). En remplaant attributes par une Map thread-safe (Hashtable, synchronizedMap ou ConcurrentHashMap), AttributeStore peut dlguer toutes ses obligations de scurit vis--vis des threads la collection thread-safe sous-jacente, ce qui limine la ncessit dune synchronisation explicite dans AttributeStore, rduit la porte du verrou la dure de laccs lobjet Map et supprime le risque quun futur dveloppeur sape la thread safety en oubliant de prendre le bon verrou avant daccder attributes.

Bien que la rduction des blocs synchronized permette damliorer ladaptabilit, un bloc synchronized peut tre trop petit il faut que les oprations qui doivent tre atomiques (comme la modication de plusieurs variables participant un invariant) soient contenues dans un mme bloc synchronized. Par ailleurs, le cot de la synchronisation ntant pas nul, dcouper un bloc synchronized en plusieurs peut, un moment donn, devenir contre-productif en termes de performances 1. Lquilibre idal dpend, bien sr, de la plate-forme mais, en pratique, il est raisonnable de ne se soucier de la taille dun bloc synchronized que lorsque lon peut dplacer dimportants calculs ou des blocs doprations en dehors de ce bloc. 11.4.2 Rduire la granularit du verrouillage Lautre moyen de rduire la dure dun verrouillage (et donc la probabilit de la comptition pour ce verrou) est de faire en sorte que les threads le demandent moins souvent. Pour cela, on peut utiliser la division ou le dcoupage des verrous, qui permettent dutiliser des verrous distincts pour protger les diffrentes variables auparavant protges par un verrou unique. Ces techniques aident rduire la granularit du verrouillage et autorisent une meilleure adaptabilit cependant, utiliser plus de verrous augmente galement le risque dinterblocages.
1. Si la JVM effectue un paississement de verrou, elle peut de toute faon annuler la division des blocs synchronized.

Chapitre 11

Performances et adaptabilit

241

titre dexprience, imaginons ce qui se passerait sil ny avait quun seul verrou pour toute lapplication au lieu dun verrou distinct pour chaque objet. Lexcution de tous les blocs synchronized, quels que soient leurs verrous, seffectuerait en srie. Si de nombreux threads se disputent le verrou global, le risque que deux threads veuillent prendre ce verrou en mme temps augmente, ce qui accrot la comptition. Si les demandes de verrouillage taient partages sur un plus grand ensemble de verrous, il y aurait moins de comptition. Moins de threads seraient bloqus en attente des verrous et ladaptabilit sen trouverait amliore. Si un verrou protge plusieurs variables indpendantes, il doit tre possible damliorer ladaptabilit en le divisant en plusieurs verrous protgeant, chacun, une variable particulire. Chaque verrou sera donc demand moins souvent. La classe ServerStatus du Listing 11.6 montre une portion de linterface de surveillance dun serveur de base de donnes qui gre lensemble des utilisateurs connects et lensemble des requtes en cours dexcution. Lorsquun utilisateur se connecte ou se dconnecte, ou lorsquune requte commence ou nit, lobjet ServerStatus est modi en appelant la mthode add() ou remove(). Les deux types dinformations sont totalement diffrents ; ServerStatus pourrait mme tre divise en deux classes sans perdre de fonctionnalits.
Listing 11.6 : Candidat au dcoupage du verrou.
@ThreadSafe public class ServerStatus { @GuardedBy("this") public final Set<String> users; @GuardedBy("this") public final Set<String> queries; ... public synchronized void addUser(String u) { users.add(u); } public synchronized void addQuery(String q) { queries.add(q); } public synchronized void removeUser(String u) { users.remove(u); } public synchronized void removeQuery(String q) { queries.remove(q); } }

Au lieu de protger users et queries avec le verrou de ServerStatus, nous pouvons protger chacun deux par un verrou distinct, comme dans le Listing 11.7. Aprs la division du verrou, chaque nouveau verrou plus n verra moins de trac que le verrou initial, plus pais (utiliser une implmentation de Set thread-safe pour users et queries au lieu demployer une synchronisation explicite produirait implicitement une division du verrou car chaque Set utiliserait un verrou diffrent pour protger son tat). Diviser un verrou en deux offre la meilleure possibilit damlioration lorsque celui-ci est moyennement disput. La division de verrous qui sont peu disputs produit peu damlioration en termes de performances ou de dbit, bien que cela puisse augmenter le seuil de charge partir duquel les performances commenceront chuter cause de la comptition.

242

Vivacit, performances et tests

Partie III

Diviser des verrous moyennement disputs peut mme en faire des verrous quasiment sans comptition, ce qui est lidal pour les performances et ladaptabilit.
Listing 11.7 : Modification de ServerStatus pour utiliser des verrous diviss.
@ThreadSafe public class ServerStatus { @GuardedBy("users") public final Set<String> users; @GuardedBy("queries") public final Set<String> queries; ... public void addUser(String u) { synchronized (users) { users.add(u); } } public void addQuery(String q) { synchronized (queries) { queries.add(q); } } // Autres mthodes modifies pour utiliser les verrous diviss. }

11.4.3 Dcoupage du verrouillage Diviser en deux un verrou trs disput produira srement deux verrous trs disputs. Bien que cela puisse apporter une petite amlioration de ladaptabilit en autorisant deux threads sexcuter en parallle au lieu dun seul, cela namliore pas normment les perspectives de concurrence sur un systme ayant de nombreux processeurs. Lexemple de division du verrou de la classe ServerStatus noffre pas dopportunit vidente pour diviser nouveau chaque verrou. La division des verrous peut parfois tre tendue en un partitionnement du verrouillage sur un ensemble de taille variable dobjets indpendants, auquel cas on parle de dcoupage du verrouillage. Limplmentation de ConcurrentHashMap, par exemple, utilise un tableau de 16 verrous protgeant chacun un seizime des entres du hachage ; lentre N est protge par le verrou N mod 16. Si lon suppose que la fonction de hachage fournit une rpartition raisonnable et quon accde aux cls de faon uniforme, cela devrait rduire la demande de chaque verrou dun facteur de 16 environ. Cest cette technique qui permet ConcurrentHashMap de supporter jusqu 16 crivains concurrents (le nombre de verrous pourrait tre augment pour fournir une concurrence encore meilleure sur les systmes ayant un grand nombre de processeurs, mais le nombre de verrous ne doit dpasser 16 que si lon est sr que les crivains concurrents sont sufsamment en comptition). Lun des inconvnients du dcoupage des verrous est quil devient plus difcile et plus coteux de verrouiller la collection en accs exclusif quavec un seul verrou. Gnralement, une opration peut seffectuer en prenant au plus un verrou mais, parfois, vous devrez verrouiller toute la collection lorsque ConcurrentHashMap doit agrandir le

Chapitre 11

Performances et adaptabilit

243

hachage et donc recalculer les valeurs des cls, par exemple. En rgle gnrale, on prend alors tous les verrous de lensemble1. La classe StripedMap du Listing 11.8 illustre limplmentation dun map laide du dcoupage de verrouillage. Chacun des N_LOCKS protge un sous-ensemble dentres. La plupart des mthodes, comme get(), nont besoin de prendre quun seul verrou dentre ; certaines peuvent devoir prendre tous les verrous mais, comme le montre clear(), pas forcment en mme temps2.
Listing 11.8 : Hachage utilisant le dcoupage du verrouillage.
@ThreadSafe public class StripedMap { // Politique de synchronisation : buckets[n] protg par // locks[n % N_LOCKS] private static final int N_LOCKS = 16; private final Node[] buckets; private final Object[] locks; private static class Node { ... } public StripedMap(int numBuckets) { buckets = new Node[numBuckets]; locks = new Object[N_LOCKS]; for (int i = 0; i < N_LOCKS; i++) locks[i] = new Object(); } private final int hash(Object key) { return Math.abs(key.hashCode() % buckets.length); } public Object get(Object key) { int hash = hash(key); synchronized (locks[hash % N_LOCKS]) { for (Node m = buckets[hash]; m != null; m = m.next) if (m.key.equals(key)) return m.value; } return null; } public void clear() { for (int i = 0; i < buckets.length; i++) { synchronized (locks[i % N_LOCKS]) { buckets[i] = null; } } } ... }

1. La seule faon de prendre un ensemble quelconque de verrous internes consiste utiliser la rcursivit. 2. Nettoyer la Map de cette faon ntant pas atomique, il nexiste pas ncessairement un instant prcis o lobjet StripedMap est vraiment vide si dautre threads sont en mme temps en train de lui ajouter des lments ; rendre cette opration atomique ncessiterait de prendre en mme temps tous les verrous. Cependant, les collections concurrentes ne pouvant gnralement pas tre verrouilles en accs exclusif par les clients, le rsultat de mthodes comme size() ou isEmpty() peut, de toute faon, tre obsolte au moment o elles se terminent ; bien quil puisse tre surprenant, ce comportement est donc gnralement acceptable.

244

Vivacit, performances et tests

Partie III

11.4.4 viter les points chauds La division et le dcoupage du verrouillage peuvent amliorer ladaptabilit car ces techniques permettent plusieurs threads de manipuler des donnes diffrentes (ou des portions diffrentes de la mme structure de donnes) sans interfrer les uns avec les autres. Un programme qui bncierait dune division du verrouillage met ncessairement en vidence une comptition qui intervient plus souvent pour un verrou que pour les donnes protges par ce verrou. Si un verrou protge deux variables indpendantes X et Y et que le thread A veuille accder X pendant que B veut accder Y (comme cela serait le cas si un thread appelait addUser() pendant quun autre appelle addQuery() dans ServerStatus), les deux threads ne seraient en comptition pour aucune donne, bien quils le soient pour un verrou. La granularit du verrouillage ne peut pas tre rduite lorsque des variables sont ncessaires pour chaque opration. Cest encore un autre domaine o les performances brutes et ladaptabilit sopposent ; les optimisations classiques, comme la mise en cache des valeurs souvent calcules, peuvent produire des "points chauds" qui limitent ladaptabilit. Si vous aviez implment HashMap, vous auriez eu le choix du calcul utilis par size() pour renvoyer le nombre dentres dans le Map. Lapproche la plus simple compte le nombre dentres chaque fois que la mthode est appele. Une optimisation classique consiste modier un compteur spar mesure que des entres sont ajoutes ou supprimes ; cela augmente lgrement le cot dune opration dajout et de suppression mais rduit celui de lopration size(), qui passe de O(n) O(1). Utiliser un compteur spar pour acclrer des oprations comme size() et isEmpty() fonctionne parfaitement pour les implmentations monothreads ou totalement synchronises, mais cela complique beaucoup lamlioration de ladaptabilit car chaque opration qui modie le hachage doit maintenant mettre jour le compteur partag. Mme en utilisant un dcoupage du verrouillage pour les chanes de hachage, la synchronisation de laccs au compteur rintroduit les problmes dadaptabilit des verrous exclusifs. Ce qui semblait tre une optimisation des performances mettre en cache le rsultat de lopration size() sest transform en un vritable boulet pour ladaptabilit. Ici, le compteur est un point chaud car chaque opration de modication doit y accder.
ConcurrentHashMap vite ce problme car sa mthode size() numre les dcoupages en additionnant les nombres de leurs lments au lieu de grer un compteur global. Pour viter de compter les lments dans chaque dcoupage, ConcurrentHashMap gre un compteur pour chacun deux, qui est galement protg par le verrou du dcoupage 1.
1. Si size() est souvent appele par rapport aux oprations de modication, les structures de donnes dcoupes peuvent optimiser son traitement en mettant en cache la taille de la collection dans une variable volatile chaque fois que la mthode est appele et en invalidant le cache (en le mettant 1) lorsque la collection est modie. Si la valeur en cache est positive lentre dans size(), cest quelle est exacte et quelle peut tre renvoye ; sinon elle est recalcule.

Chapitre 11

Performances et adaptabilit

245

11.4.5 Alternatives aux verrous exclusifs Une troisime technique pour attnuer leffet de la comptition sur un verrou consiste renoncer aux verrous exclusifs au prot de mcanismes de gestion de ltat partag plus adapts la concurrence, comme les collections concurrentes, les verrous de lecture/ criture, les objets non modiables et les variables atomiques. La classe ReadWriteLock du Chapitre 13 met en application une discipline "plusieurs lecteurs-un seul crivain" : plusieurs lecteurs peuvent accder simultanment la ressource partage du moment quaucun dentre eux ne veut la modier ; les crivains, en revanche, doivent prendre un verrou exclusif. Pour les structures de donnes qui sont essentiellement lues, ReadWriteLock permet une plus grande concurrence que le verrouillage exclusif ; pour les structures de donnes en lecture seule, limmuabilit limine totalement le besoin dun verrou. Les variables atomiques (voir Chapitre 15) permettent de rduire le cot de modication des "points chauds" comme les compteurs, les gnrateurs de squences ou la rfrence au premier lien dune structure de donnes chane (nous avons dj utilis AtomicLong pour grer le compteur de visites dans lexemple des servlets du Chapitre 2). Les classes de variables atomiques fournissent des oprations atomiques trs nes (et donc plus adaptables) sur les entiers ou les rfrences dobjets et sont implmentes laide des primitives de concurrence de bas niveau (comme compare-et-change), disponibles sur la plupart des processeurs modernes. Si une classe possde un petit nombre de points chauds qui ne participent pas aux invariants avec dautres variables, les remplacer par des variables atomiques permettra damliorer ladaptabilit (un changement dalgorithme pour avoir moins de points chauds peut lamliorer encore plus les variables atomiques rduisent le cot de la modication des points chauds mais ne lliminent pas). 11.4.6 Surveillance de lutilisation du processeur Lorsque lon teste ladaptabilit, le but est gnralement de faire en sorte que les processeurs soient utiliss au maximum. Des outils comme vmstat et mpstat avec Unix ou perfmon avec Windows permettent de connatre le taux dutilisation des processeurs. Si ces derniers sont utiliss de faon asymtrique (certains processeurs sont au maximum de leur charge alors que dautres ne le sont pas), le premier but devrait tre damliorer le paralllisme du programme. Une utilisation asymtrique indique en effet que la plupart des calculs sexcutent dans un petit ensemble de threads et que lapplication ne pourra pas tirer parti de processeurs supplmentaires. Si les processeurs ne sont pas totalement utiliss, il faut en trouver la raison. Il peut y avoir plusieurs causes :
m

Charge insufsante. Lapplication teste nest peut-tre pas soumise une charge sufsante. Cela peut tre vri en augmentant la charge et en mesurant limpact sur lutilisation, le temps de rponse ou le temps de service. Produire une charge

246

Vivacit, performances et tests

Partie III

sufsante pour saturer une application peut ncessiter une puissance de traitement non ngligeable ; le problme peut tre que les systmes clients, pas celui qui est test, sexcutent avec cette capacit.
m

Liaison aux E/S. Pour dterminer si une application est lie au disque, on peut utiliser iostat ou perfmon et surveiller le niveau du trac sur le rseau pour savoir si elle est limite par la bande passante. Liaison externe. Si lapplication dpend de services externes comme une base de donnes ou un service web, le goulet dtranglement nest peut-tre pas dans son code. Pour le tester, on utilise gnralement un proleur ou les outils dadministration de la base de donnes pour connatre le temps pass attendre les rponses du service externe. Comptition pour les verrous. Les outils danalyse permettent de se rendre compte de la comptition laquelle sont soumis les verrous de lapplication et les verrous qui sont "chauds". On peut souvent obtenir ces informations via un chantillonnage alatoire, en dclenchant quelques traces de threads et en examinant ceux qui combattent pour laccs aux verrous. Si un thread est bloqu en attente dun verrou, le cadre de pile correspondant dans la trace de thread indiquera "waiting to lock monitor ... ". Les verrous qui sont peu disputs apparatront rarement dans une trace de thread alors que ceux qui sont trs demands auront toujours au moins un thread en attente et seront donc souvent prsents dans la trace.

Si lapplication garde les processeurs sufsamment occups, on peut utiliser des outils de surveillance pour savoir sil serait bnque den ajouter dautres. Un programme qui na que quatre threads peut occuper totalement un systme quatre processeurs, mais il est peu probable quon note une amlioration des performances en passant un systme huit processeurs puisquil faudrait quil y ait des threads excutables en attente pour tirer parti des processeurs supplmentaires (il est galement possible de recongurer le programme pour diviser sa charge de travail sur plus de threads, en ajustant par exemple la taille du pool de threads). Lune des colonnes du rsultat de vmstat est le nombre de threads excutables mais qui ne sexcutent pas parce quil ny a pas de processeur disponible ; si lutilisation du CPU est leve et quil y ait toujours des threads excutables qui attendent un processeur, lapplication bnciera srement de lajout de processeurs supplmentaires. 11.4.7 Dire non aux pools dobjets Dans les premires versions de la JVM, lallocation des objets et le ramasse-miettes taient des oprations lentes1 mais leurs performances se sont beaucoup amliores depuis. En fait, lallocation en Java est dsormais plus rapide que malloc() en C : la portion de
1. Comme tout le reste synchronisation, graphisme, lancement de la JVM, introspection lest immanquablement dans la premire version dune technologie exprimentale.

Chapitre 11

Performances et adaptabilit

247

code commune entre linstruction new Object de HotSpot 1.4.x et celle de la version 5.0 est denviron dix instructions machine. Pour contourner les cycles de vie "lents" des objets, de nombreux dveloppeurs se sont tourns vers les pools dobjets, o les objets sont recycls au lieu dtre supprims par le ramasse-miettes et allous de nouveau si ncessaire. Mme si lon prend en compte leur surcot rduit du ramassage des objets, il a t prouv que dans les programmes monothreads les pools dobjets diminuaient les performances1 pour tous les objets, sauf les plus coteux (et cette perte de performance est trs importante pour les objets lgers et moyens) (Click, 2005). La situation est encore pire avec les applications concurrentes. Lorsque les threads allouent de nouveaux objets, il ny a besoin que de trs peu de coordination entre eux car les allocateurs utilisent gnralement des blocs dallocation locaux aux threads an dliminer la plupart de la synchronisation sur les structures de donnes du tas. Si un thread demande un objet du pool, en revanche, il faut une synchronisation pour coordonner laccs la structure de donnes du pool, ce qui introduit la possibilit de blocage du thread. Le blocage dun thread cause de la comptition sur un verrou tant des centaines de fois plus coteux quune allocation, mme une petite comptition introduite par un pool serait un goulet dtranglement pour ladaptabilit (une synchronisation sans comptition est gnralement plus coteuse que lallocation dun objet). Il sagit donc encore dune autre technique conue pour amliorer les performances, mais qui sest traduite en risque pour ladaptabilit. Les pools ont leur utilit 2, mais pas pour amliorer les performances.
Allouer des objets est gnralement moins coteux que la synchronisation.

11.5

Exemple : comparaison des performances des Map

Dans un programme monothread, les performances de ConcurrentHashMap sont lgrement meilleures que celles dun HashMap synchronis, mais cest dans un contexte concurrent que sa suprmatie apparat. Limplmentation de ConcurrentHashMap supposant que lopration la plus courante est la rcupration dune valeur existante, il est donc optimis
1. Outre quils provoquent une perte des performances en termes de cycles processeurs, les pools dobjets souffrent dun certain nombre dautres problmes, dont la difcult de congurer correctement les tailles des pools (sils sont trop petits, ils nont aucun effet ; sils sont trop grands, ils surchargent le ramasse-miettes et monopolisent de la mmoire qui aurait pu tre utilise plus efcacement pour autre chose) ; le risque quun objet ne soit pas correctement rinitialis dans son nouvel tat (ce qui introduit des bogues subtils) ; le risque quun thread renvoie un objet au pool tout en continuant lutiliser. Enn, les pools dobjets demandent plus de travail aux ramasse-miettes gnrationnels car ils encouragent un motif de rfrences ancien-vers-nouveau. 2. Dans les environnements contraints, comme certaines cibles J2ME ou RTSJ, les pools dobjets peuvent quand mme tre ncessaires pour grer la mmoire de faon efcace ou pour contrler la ractivit.

248

Vivacit, performances et tests

Partie III

pour fournir les meilleures performances et la concurrence la plus leve dans le cas doprations get() russies. Le principal obstacle limplmentation des Map synchronises est quil ny a quun seul verrou pour tout lobjet et quun seul thread peut donc y accder un instant donn. ConcurrentHashMap, en revanche, ne verrouille pas la plupart des oprations de lecture russies et utilise le dcoupage des verrous pour les oprations dcriture et les rares oprations de lecture qui ont besoin du verrouillage. Plusieurs threads peuvent donc accder simultanment lobjet Map sans risquer de se bloquer. La Figure 11.3 illustre les diffrences dadaptabilit entre plusieurs implmentations de Map : ConcurrentHashMap, ConcurrentSkipListMap et HashMap et TreeMap enveloppes avec synchronizedMap(). Les deux premires sont thread-safe de par leur conception ; les deux dernires le sont grce lenveloppe de synchronisation. Dans chaque test, N threads excutent simultanment une boucle courte qui choisit une cl au hasard et tente de rcuprer la valeur qui lui correspond. Si cette valeur nexiste pas, elle est ajoute lobjet Map avec la probabilit p = 0,6 ; si elle existe, elle est supprime avec la probabilit p = 0,02. Les tests se sont drouls avec une version de dveloppement de Java 6 sur une machine Sparc V880 huit processeurs et le graphique afche le dbit normalis par rapport au dbit dun seul thread avec ConcurrentHashMap (avec Java 5.0, lcart dadaptabilit est encore plus grand entre les collections concurrentes et synchronises). Les rsultats de ConcurrentHashMap et ConcurrentSkipListMap montrent que ces classes sadaptent trs bien un grand nombre de threads ; le dbit continue en effet daugmenter mesure que lon en ajoute. Bien que le nombre de threads de la Figure 11.3 ne semble pas lev, ce programme de test produit plus de comptition par thread quune application classique car il se contente essentiellement de maltraiter lobjet Map ; un programme normal ajouterait des traitements locaux aux threads lors de chaque itration. Les rsultats des collections synchronises ne sont pas aussi encourageants. Avec un seul thread, les performances sont comparables celles de ConcurrentHashMap mais, ds que la charge passe dune situation essentiellement sans comptition une situation trs dispute ce qui a lieu ici avec deux threads , les collections synchronises souffrent beaucoup. Cest un comportement classique des codes dont ladaptabilit est limite par la comptition sur les verrous. Tant que cette comptition est faible, le temps par opration est domin par celui du traitement et le dbit peut augmenter en mme temps que le nombre de threads. Lorsque la comptition devient importante, ce sont les dlais des changements de contexte et de la planication qui lemportent sur le temps de traitement ; ajouter des threads a alors peu deffet sur le dbit.

Chapitre 11

Performances et adaptabilit

249

Figure 11.3
Comparaison de ladaptabilit des implmentations de Map.

4 ConcurrentHashMap

Dbit (normalis)

ConcurrentSkipListMap

1 0 1 2 4 8 16 32 Nombre de threads synchronized HashMap synchronized TreeMap 64

11.6

Rduction du surcot des changements de contexte

De nombreuses tches utilisent des oprations qui peuvent se bloquer ; le passage de ltat "en cours dexcution" "bloqu" implique un changement de contexte. Dans les applications serveur, une source de blocage est la production des messages du journal au cours du traitement des requtes ; pour illustrer comment il est possible damliorer le dbit en rduisant ces changements de contexte, nous analyserons le comportement des deux approches pour lcriture de ces messages. La plupart des frameworks de journalisation ne font quenvelopper lgrement println() ; lorsquil faut inscrire quelque chose dans le journal, on se contente dy crire. La classe LogWriter du Listing 7.13 prsentait une autre approche : linscription des messages seffectuait dans un thread ddi en arrire-plan et non dans le thread demandeur. Du point de vue du dveloppeur, ces deux approches sont peu prs quivalentes, mais elles peuvent avoir des performances diffrentes en fonction du volume de lactivit de journalisation, du nombre de threads qui inscrivent des messages et dautres facteurs comme le cot des changements de contexte1. Le temps de service dune inscription dans le journal inclut tous les calculs associs aux classes des ux dE/S ; si lopration dE/S est bloque, il comprend galement la dure de blocage du thread. Le systme dexploitation dprogrammera le thread bloqu jusqu ce que lE/S se termine probablement un peu plus longtemps. la n de lE/S, les threads qui sont actifs seront autoriss terminer leur quantum de temps ; les threads qui sont dj en attente sajouteront au temps de service. Une comptition pour
1. La construction dun logger qui dplace les E/S vers un autre thread permet damliorer les performances mais introduit galement un certain nombre de complications pour la conception, comme les interruptions (que se passera-t-il si un thread bloqu dans une opration avec le journal est interrompu ?), les garanties de service (le logger garantit-il quun message plac dans la le dattente du journal sera inscrit dans celui-ci avant larrt du service ?), la politique de saturation (que se passera-t-il lorsque les producteurs inscriveront des messages dans le journal plus vite que le thread logger ne peut les traiter ?) et le cycle de vie du service (comment teindre le logger et comment communiquer ltat du service aux producteurs ?).

250

Vivacit, performances et tests

Partie III

laccs au verrou du ux de sortie peut survenir si plusieurs threads envoient simultanment des messages au journal, auquel cas le rsultat sera le mme que pour les E/S bloquantes les threads se bloquent sur le verrou et sont dprogramms. Une journalisation en ligne implique des E/S et des verrous, ce qui peut conduire une augmentation des changements de contexte et donc un temps de service plus long. Laugmentation du temps de service nest pas souhaitable pour plusieurs raisons. La premire est que le temps de service affecte la qualit du service un temps plus long signie que lon attendra plus longtemps le rsultat , mais ce qui est plus grave est que des temps de service plus longs signient ici plus de comptition pour les verrous. Le principe "rentre et sort" de la section 11.4.1 nous enseigne quil faut dtenir les verrous le moins longtemps possible car plus le verrouillage dure, plus il y aura de comptition. Si un thread se bloque en attente dune E/S pendant quil dtient un verrou, un autre thread voudra srement le prendre pendant que le premier le dtient. Les systmes concurrents fonctionnent bien mieux lorsque les verrous ne sont pas disputs car lacquisition dun verrou trs demand implique plus de changements de contexte, or un style de programmation qui encourage plus de changements de contexte produit un dbit global plus faible. Dplacer les E/S en dehors du thread de traitement de la requte raccourcira le temps de service moyen. Les threads qui crivent dans le journal ne se bloqueront plus en attente du verrou du ux de sortie ou de la n de lopration dcriture ; ils nont besoin que de placer le message dans la le dattente avant de revenir leur traitement. Cela introduit videmment une comptition possible pour la le dattente, mais lopration put() est plus lgre que lcriture dans le journal (qui peut ncessiter des appels systmes) et risque donc moins de se bloquer (du moment que la le nest pas pleine). Le thread de traitement de la requte se bloquera donc moins et il y aura moins de changements de contexte au beau milieu dune requte. Nous avons donc transform une portion de code complique et incertaine, impliquant des E/S et une possible comptition pour les verrous, en une portion de code en ligne droite. Dans un certain sens, nous navons fait que contourner le problme en dplaant lE/S dans un thread dont le cot nest pas perceptible par lutilisateur (ce qui, en soi, est une victoire). Mais, en dplaant dans un seul thread toutes les critures dans le journal, nous avons galement limin le risque de comptition pour le ux de sortie et donc une source de blocage, ce qui amliore le dbit global puisque moins de ressources sont consommes en planication, changements de contexte et gestion des verrous. Dplacer les E/S des nombreux threads de traitement des requtes dans un seul thread de gestion du journal produit la mme diffrence quentre une brigade de pompiers faisant une chane avec des seaux et un ensemble dindividus luttant sparment contre un feu. Dans lapproche "des centaines de personnes courant avec des seaux", il y a plus de risque de comptition pour la source deau et pour laccs au feu (il y aura donc globalement moins deau dverse sur le feu). Lefcacit est galement moindre puisque

Chapitre 11

Performances et adaptabilit

251

chaque personne change continuellement de mode (remplissage, course, vidage, course, etc.). Dans lapproche de la brigade de pompiers, le ux de leau allant de la source vers lincendie est constant, moins dnergie est dpense pour transporter leau vers le feu et chaque intervenant se dvoue continuellement la mme tche. Tout comme les interruptions perturbent et rduisent la productivit humaine, les blocages et les changements de contexte perturbent les threads.

Rsum
Lune des raisons les plus frquentes dutiliser les threads est dexploiter plusieurs processeurs. Lorsque lon tudie les performances des applications concurrentes, nous nous intressons donc gnralement plus au dbit et ladaptabilit quau temps de service brut. La loi dAmdahl nous indique que ladaptabilit dune application est gouverne par la proportion de code qui doit sexcuter en srie. La source essentielle de la srialisation dans les programmes Java tant le verrouillage exclusif des ressources, nous pouvons souvent amliorer ladaptabilit en dtenant les verrous pendant moins longtemps, soit en rduisant la granularit du verrouillage, soit en remplaant les verrous exclusifs par des alternatives non exclusives ou non bloquantes.

12
Tests des programmes concurrents
Les programmes concurrents emploient des principes et des patrons de conception semblables ceux des programmes squentiels. La diffrence est que, contrairement ces derniers, leur lger non-dterminisme augmente le nombre dinteractions et de types derreurs possibles, qui doivent tre prvues et analyses. Les tests des programmes concurrents utilisent et tendent les ides des tests des programmes squentiels. On utilise donc les mmes techniques pour tester la justesse et les performances, mais lespace de problmes possibles est bien plus vaste avec les programmes concurrents quavec les programmes squentiels. Le plus grand d des tests des programmes concurrents est que certaines erreurs peuvent tre rares et alatoires au lieu dtre dterministes ; pour les trouver, les tests doivent donc tre plus pousss et sexcuter plus longtemps que les tests squentiels classiques. La plupart des tests des classes concurrentes appartiennent aux deux catgories classiques de la scurit et de la vivacit. Au Chapitre 1, nous avons dni la premire comme "rien de mauvais ne se passera jamais" et la seconde comme "quelque chose de bon nira par arriver". Les tests de la scurit, qui vrient que le comportement dune classe est conforme sa spcication, consistent gnralement tester des invariants. Dans une implmentation de liste chane qui met en cache la taille de la liste chaque fois quelle est modie, par exemple, un test de scurit consisterait comparer la taille en cache avec le nombre actuel des lments de la liste. Dans un programme monothread, ce test est simple puisque le contenu de la liste ne change pas pendant que lon teste ses proprits. Dans un programme concurrent, en revanche, ce genre de test peut tre perverti par des situations de comptition, sauf si la lecture de la valeur en cache et le comptage des lments seffectuent dans une seule opration atomique. Pour ce faire, on peut verrouiller la liste pour disposer dun accs exclusif en se servant dune sorte dinstantan atomique fourni par limplmentation ou de "points de tests" permettant de vrier les invariants

254

Vivacit, performances et tests

Partie III

ou dexcuter du code de test de faon atomique. Dans ce livre, nous avons utilis des diagrammes temporels pour reprsenter les interactions "malheureuses" pouvant causer des erreurs avec les classes mal construites ; les programmes de tests tentent dexaminer sufsamment lespace des tats pour que ces situations malheureuses nissent par arriver. Malheureusement, le code de test peut introduire des artefacts de timing ou de synchronisation qui peuvent masquer des bogues qui auraient pu se manifester deux-mmes 1. Les tests de la vivacit doivent relever leurs propres ds car ils comprennent les tests de progression et de non-progression, qui sont difciles quantier comment vrier, par exemple, quune mthode est bloque et quelle ne sexcute pas simplement lentement ? De mme, comment tester quun algorithme ne provoquera pas un interblocage ? Pendant combien de temps faut-il attendre avant de dclarer quil a chou ? Les tests de performances sont lis aux tests de vivacit. Les performances peuvent se mesurer de diverses faons, notamment :
m m

Le dbit. La vitesse laquelle un ensemble de tches concurrentes se termine. La ractivit. Le dlai entre une requte et lachvement de laction (galement appele latence). Ladaptabilit. Lamlioration du dbit (ou non) lorsque lon dispose de ressources supplmentaires (gnralement des processeurs).

12.1

Tests de la justesse

Le dveloppement de tests unitaires pour une classe concurrente commence par la mme analyse que pour une classe squentielle on identie les invariants et les postconditions qui peuvent tre tests mcaniquement. Avec un peu de chance, la plupart sont dcrits dans la spcication ; le reste du temps, lcriture des tests est une aventure dans la dcouverte itrative des spcications. titre dexemple, nous allons construire un ensemble de cas de tests pour un tampon born. Le Listing 12.1 contient le code de la classe BoundedBuffer, qui utilise Semaphore pour borner le tampon et pour viter les blocages. BoundedBuffer implmente une le reposant sur un tableau de taille xe et fournissant des mthodes put() et take() contrles par une paire de smaphores. Le smaphore availableItems reprsente le nombre dlments pouvant tre ts du tampon ; il vaut initialement zro (puisque le tampon est vide au dpart). De mme, availableSpaces reprsente le nombre dlments pouvant tre ajouts au tampon ; il est initialis avec la taille du tampon.

1. Les bogues qui disparaissent lorsque lon dbogue ou que lon ajoute du code de test sont appels Heisen bogues.

Chapitre 12

Tests des programmes concurrents

255

Listing 12.1 : Tampon born utilisant la classe Semaphore.


@ThreadSafe public class BoundedBuffer<E> { private final Semaphore availableItems, availableSpaces; @GuardedBy("this") private final E[] items; @GuardedBy("this") private int putPosition = 0, takePosition = 0; public BoundedBuffer(int capacity) { availableItems = new Semaphore(0); availableSpaces = new Semaphore(capacity); items = (E[]) new Object[capacity]; } public boolean isEmpty() { return availableItems.availablePermits() == 0; } public boolean isFull() { return availableSpaces.availablePermits() == 0; } public void put(E x) throws InterruptedException { availableSpaces.acquire(); doInsert(x); availableItems.release(); } public E take() throws InterruptedException { availableItems.acquire(); E item = doExtract(); availableSpaces.release(); return item; } private synchronized void doInsert(E x) { int i = putPosition; items[i] = x; putPosition = (++i == items.length)? 0: i; } private synchronized E doExtract() { int i = takePosition; E x = items[i]; items[i] = null; takePosition = (++i == items.length)? 0 : i; return x; } }

Une opration take() ncessite dabord dobtenir un permis de la part de available Items, ce qui russit tout de suite si le tampon nest pas vide mais qui, sinon, bloque lopration jusqu ce quil ne soit plus vide. Lorsque ce permis a t obtenu, llment suivant du tampon est t et on dlivre un permis au smaphore availableSpaces1. Lopration put() a le fonctionnement inverse ; la sortie des mthodes put() ou take(), la somme des compteurs des deux smaphores est toujours gale la taille du tableau (en pratique, il vaudrait mieux utiliser la classe ArrayBlockingQueue ou Linked BlockingQueue pour implmenter un tampon born plutt quen construire un soi-mme,
1. Dans un smaphore, les permis ne sont pas reprsents explicitement ni associs un thread particulier ; une opration release() cre un permis et une opration acquire() en consomme un.

256

Vivacit, performances et tests

Partie III

mais la technique utilise ici illustre galement comment contrler les insertions et les suppressions dans dautres structures de donnes). 12.1.1 Tests unitaires de base Les tests unitaires les plus basiques pour BoundedBuffer ressemblent ceux que lon utiliserait dans un contexte squentiel cration dun tampon born, appel ses mthodes et assertions sur les postconditions et les invariants. Parmi ces derniers, on pense immdiatement au fait quun tampon venant dtre cr devrait savoir quil est vide et quil nest pas plein. Un test de scurit similaire mais un peu plus compliqu consiste insrer N lments dans un tampon de capacit N (ce qui devrait seffectuer sans blocage) et tester que le tampon reconnaisse quil est plein (et non vide). Les mthodes de tests Junit pour ces proprits sont prsentes dans le Listing 12.2.
Listing 12.2 : Tests unitaires de base pour BoundedBuffer.
class BoundedBufferTest extends TestCase { void testIsEmptyWhenConstructed() { BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>(10); assertTrue(bb.isEmpty()); assertFalse(bb.isFull()); } void testIsFullAfterPuts() throws InterruptedException { BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>(10); for (int i = 0; i < 10; i++) bb.put(i); assertTrue(bb.isFull()); assertFalse(bb.isEmpty()); } }

Ces mthodes de tests simples sont entirement squentielles. Il est souvent utile dinclure un ensemble de tests squentiels dans une suite de tests car ils permettent de dvoiler les problmes indpendants de la concurrence avant de commencer rechercher les situations de comptition. 12.1.2 Tests des oprations bloquantes Les tests des proprits essentielles de la concurrence ncessitent dintroduire plusieurs threads. La plupart des frameworks de tests ne sont pas particulirement conus pour cela : ils facilitent rarement la cration de threads ou leur surveillance pour vrier quils ne meurent pas de faon inattendue. Si un thread auxiliaire cr par un test dcouvre une erreur, le framework ne sait gnralement pas quel test ce thread est attach et un travail supplmentaire est donc ncessaire pour relayer le succs ou lchec au thread principal des tests, an quil puisse le signaler. Pour les tests de conformit de java.util.concurrent, il tait important que les checs soient clairement associs un test spcique. Le JSR 166 Expert Group a donc

Chapitre 12

Tests des programmes concurrents

257

cr une classe de base1 qui fournissait des mthodes pour relayer et signaler les erreurs au cours de tearDown() en respectant la convention que chaque test devait attendre que tous les threads quil avait crs se terminent. Vous navez peut-tre pas besoin daller jusqu de telles extrmits ; lessentiel est de savoir clairement si les tests ont eu lieu et que les informations sur les checs soient signales quelque part, an que lon puisse les utiliser pour diagnostiquer le problme. Si une mthode est cense se bloquer sous certaines conditions, un test de ce comportement ne devrait russir que si le thread est bloqu : tester quune mthode se bloque revient tester quelle lance une exception ; si son excution se poursuit, cest que le test a chou. Tester quune mthode se bloque introduit une complication supplmentaire : lorsque la mthode sest correctement bloque, vous devez la convaincre de se dbloquer. La faon la plus vidente de le faire consiste utiliser une interruption lancer une activit bloquante dans un thread distinct, attendre que le thread se bloque, linterrompre, puis tester que lopration bloquante sest termine. Ceci ncessite videmment que les mthodes bloquantes rpondent aux interruptions en se terminant prmaturment ou en lanant lexception InterruptedException. La partie "attendre que le thread se bloque" est plus facile dire qu faire ; en pratique, il faut prendre une dcision arbitraire sur la dure que peuvent avoir les quelques instructions excutes et attendre plus longtemps. Vous devez vous prparer augmenter ce dlai si vous vous tes tromp (auquel cas vous constaterez de faux checs des tests). Le Listing 12.3 montre une approche permettant de tester les oprations bloquantes. La mthode cre un thread taker qui tente de prendre un lment dans un tampon vide. Si cette opration russit, il constate lchec. Le thread qui excute lance taker et attend un certain temps avant de linterrompre. Si taker sest correctement bloqu dans lopration take(), il lancera une exception InterruptedException ; le bloc catch correspondant la traitera comme un succs et permettra au thread de se terminer. Le thread de test principal attend alors la n de taker avec join() et vrie que cet appel a russi en appelant Thread.isAlive() ; si le thread taker a rpondu linterruption, lappel join() devrait se terminer rapidement.
Listing 12.3 : Test du blocage et de la rponse une interruption.
void testTakeBlocksWhenEmpty() { final BoundedBuffer<Integer> bb = new BoundedBuffer <Integer>(10); Thread taker = new Thread() { public void run() { try { int unused = bb.take(); fail(); // if we get here, its an error } catch (InterruptedException success) { } }}; try {

1. Voir http://gee.cs.oswego.edu/cgi- bin/viewcvs.cgi/jsr166/src/test/tck/JSR166TestCase.java.

258

Vivacit, performances et tests

Partie III

Listing 12.3 : Test du blocage et de la rponse une interruption. (suite)


taker.start(); Thread.sleep(LOCKUP_DETECT_TIMEOUT ); taker.interrupt(); taker.join(LOCKUP_DETECT_TIMEOUT ); assertFalse(taker.isAlive()); } catch (Exception unexpected) { fail(); } }

La jointure avec dlai garantit que le test se terminera mme si take() se ge de faon inattendue. Cette mthode teste plusieurs proprits de take() pas seulement quelle se bloque, mais galement quelle lance InterruptedException lorsquelle est interrompue. Cest lun des rares cas o il est justi de sous-classer explicitement Thread au lieu dutiliser un Runnable dans un pool puisque cela permet de tester la terminaison correcte avec join(). On peut utiliser la mme approche pour tester que taker se dbloque quand le thread principal ajoute un lment la le. Bien quil soit tentant dutiliser Thread.getState() pour vrier que le thread est bien bloqu sur lattente dune condition, cette approche nest pas able. Rien nexige en effet quun thread bloqu soit dans ltat WAITING ou TIMED_WAITING puisque la JVM peut choisir dimplmenter le blocage par une attente tournante. De mme, les faux rveils de Object.wait() ou Condition.await() tant autoriss (voir Chapitre 14), un thread dans ltat WAITING ou TIMED_WAITING peut passer temporairement dans ltat RUNNABLE, mme si la condition quil attend nest pas encore vrie. Mme en ignorant ces dtails dimplmentation, le thread cible peut mettre un certain temps pour se stabiliser dans un tat bloquant. Le rsultat de Thread.getState() ne devrait pas tre utilis pour contrler la concurrence et son utilit est limite pour les tests son principal intrt est de fournir des informations qui servent au dbogage. 12.1.3 Test de la scurit vis--vis des threads Les Listings 12.2 et 12.3 testent des proprits importantes du tampon born mais ne dcouvriront srement pas les erreurs dues aux situations de comptition. Pour tester quune classe concurrente se comporte correctement dans le cas daccs concurrents imprvisibles, il faut mettre en place plusieurs threads effectuant des oprations put() et take() pendant un certain temps et vrier que tout sest bien pass. La construction de tests pour dcouvrir les erreurs de scurit vis--vis des threads dans les classes concurrentes est un problme identique celui de luf et de la poule car les programmes de tests sont souvent eux-mmes des programmes concurrents. Dvelopper de bons tests concurrents peut donc se rvler plus difcile que lcriture des classes quils testent.

Chapitre 12

Tests des programmes concurrents

259

Le d de la construction de tests efcaces pour la scurit des classes concurrentes vis-vis des threads consiste identier facilement les proprits qui choueront srement si quelque chose se passe mal, tout en ne limitant pas articiellement la concurrence par le code danalyse des erreurs. Il est prfrable que le test dune proprit ne ncessite pas de synchronisation.

Une approche qui fonctionne bien avec les classes utilises dans les conceptions producteur-consommateur (comme BoundedBuffer) consiste vrier que tout ce qui est plac dans une le ou un tampon en ressort et rien dautre. Une application nave de cette mthode serait dinsrer llment dans une liste "fantme" lorsquil est plac dans la le, de lenlever de cette liste lorsquil est t de la le et de tester que la liste fantme est vide quand le test sest termin. Cependant, cette approche dformerait la planication des threads de test car la modication de la liste fantme ncessiterait une synchronisation et risquerait de provoquer des blocages. Une meilleure technique consiste calculer les sommes de contrle des lments mis dans la le et sortis de celle-ci en utilisant une fonction de calcul tenant compte de lordre, puis les comparer. Sils correspondent, le test a russi. Cette approche fonctionne mieux lorsquil ny a quun producteur qui place les lments dans le tampon et un seul consommateur qui les en extrait car elle peut tester non seulement que ce sont (srement) les bons lments qui sortent, mais galement quils sont extraits dans le bon ordre. Pour tendre cette mthode une situation multi-producteurs/multi-consommateurs, il faut utiliser une fonction de calcul de la somme de contrle qui ne tienne pas compte de lordre dans lequel les lments sont combins, an que les diffrentes sommes de contrle puissent tre combines aprs le test. Sinon la synchronisation de laccs un champ de contrle partag deviendrait un goulet dtranglement pour la concurrence ou perturberait le timing du test (toute opration commutative, comme une addition ou un "ou exclusif", correspond ces critres). Pour vrier que le test effectue bien les vrications souhaites, il est important que les sommes de contrle ne puissent tre anticipes par le compilateur. Ce serait donc une mauvaise ide dutiliser des entiers conscutifs comme donnes du test car le rsultat serait toujours le mme et un compilateur volu pourrait les calculer lavance. Pour viter ce problme, les donnes de test devraient tre produites alatoirement, mais un mauvais choix de gnrateur de nombre alatoire (GNA) peut compromettre de nombreux autres tests. La gnration des nombres alatoires peut, en effet, crer des couplages entre les classes et les artefacts de timing car la plupart des classes de gnration sont thread-safe et introduisent donc une synchronisation supplmentaire 1. Si
1. De nombreux tests de performances ne sont, linsu de leurs dveloppeurs ou de leurs utilisateurs, que des tests qui valuent la taille du goulet dtranglement pour la concurrence que constitue le GNA.

260

Vivacit, performances et tests

Partie III

chaque thread utilise son propre GNA, on peut utiliser un gnrateur de nombre alatoire non thread-safe. Les fonctions pseudo-alatoires sont tout faire prfrables aux gnrateurs. Il nest pas ncessaire dobtenir une haute qualit dala : il suft quil puisse garantir que les nombres changeront dune excution lautre. La fonction xorShift() du Listing 12.4 (Marsaglia, 2003) fait partie de ces fonctions peu coteuses qui fournissent des nombres alatoires de qualit moyenne. En la lanant avec des valeurs reposant sur hashCode() et nanoTime(), les sommes seront sufsamment imprvisibles et presque toujours diffrentes chaque excution.
Listing 12.4 : Gnrateur de nombre alatoire de qualit moyenne mais suffisante pour les tests.
static int xorShift(int y) { y ^= (y << 6); y ^= (y >>> 21); y ^= (y << 7); return y; }

La classe PutTakeTest des Listings 12.5 et 12.6 lance N threads producteurs qui gnrent des lments et les placent dans une le et N threads consommateurs qui les extraient. Chaque thread met jour la somme de contrle des lments mesure quils entrent ou sortent en utilisant une somme de contrle par thread qui est combine la n de lexcution du test, an de ne pas ajouter plus de synchronisation ou de comptition que ncessaire.
Listing 12.5 : Programme de test producteur-consommateur pour BoundedBuffer.
public class PutTakeTest { private static final ExecutorService pool = Executors.newCachedThreadPool(); private final AtomicInteger putSum = new AtomicInteger(0); private final AtomicInteger takeSum = new AtomicInteger(0); private final CyclicBarrier barrier; private final BoundedBuffer <Integer> bb; private final int nTrials, nPairs; public static void main(String[] args) { new PutTakeTest(10, 10, 100000).test(); // params exemple pool.shutdown(); } PutTakeTest(int capacity, int npairs, int ntrials) { this.bb = new BoundedBuffer<Integer>(capacity); this.nTrials = ntrials; this.nPairs = npairs; this.barrier = new CyclicBarrier(npairs * 2 + 1); } void test() { try { for (int i = 0; i < nPairs; i++) { pool.execute(new Producer());

Chapitre 12

Tests des programmes concurrents

261

pool.execute(new Consumer()); } barrier.await(); // Attend que tous les threads soient prts barrier.await(); // Attend que tous les threads aient fini assertEquals(putSum.get(), takeSum.get()); } catch (Exception e) { throw new RuntimeException (e); } } class Producer implements Runnable { /* Listing12.6 */ } class Consumer implements Runnable { /* Listing12.6 */ } }

Listing 12.6 : Classes producteur et consommateur utilises dans PutTakeTest.


/* Classes internes de PutTakeTest (Listing12.5) */ class Producer implements Runnable { public void run() { try { int seed = (this.hashCode() ^ (int)System.nanoTime()); int sum = 0; barrier.await(); for (int i = nTrials; i > 0; --i) { bb.put(seed); sum += seed; seed = xorShift(seed); } putSum.getAndAdd(sum); barrier.await(); } catch (Exception e) { throw new RuntimeException (e); } } } class Consumer implements Runnable { public void run() { try { barrier.await(); int sum = 0; for (int i = nTrials; i > 0; --i) { sum += bb.take(); } takeSum.getAndAdd(sum); barrier.await(); } catch (Exception e) { throw new RuntimeException(e); } } }

En fonction de la plate-forme, la cration et le lancement dun thread peuvent tre des oprations plus ou moins lourdes. Si les threads sont courts et quon en lance un certain nombre dans une boucle, ceux-ci sexcuteront squentiellement et non en parallle dans le pire des cas. Mme avant cette situation extrme, le fait que le premier thread ait une longueur davance sur les autres signie quil peut y avoir moins dentrelacements que prvu : le premier thread sexcute seul pendant un moment, puis les deux premiers sexcutent en parallle pendant un certain temps et il nest pas dit que tous les threads

262

Vivacit, performances et tests

Partie III

niront par sexcuter en parallle (le mme phnomne a lieu la n de lexcution : les threads qui ont eu une longueur davance se nissent galement plus tt). Dans la section 5.5.1, nous avions prsent une technique permettant dattnuer ce problme en utilisant un objet CountDownLatch comme porte dentre et un autre comme porte de sortie. Un autre moyen dobtenir le mme effet consiste utiliser un objet CyclicBarrier initialis avec le nombre de threads plus un et faire en sorte que les threads et le pilote de test attendent la barrire au dbut et la n de leurs excutions. Ceci garantit que tous les threads seront lancs avant quun seul ne commence son travail. Cest la technique quutilise PutTakeTest pour coordonner le lancement et larrt des threads, ce qui cre plus de possibilits dentrelacement concurrent. Nous ne pouvons quand mme pas garantir que le planicateur nexcutera pas squentiellement chaque thread mais, si ces excutions sont sufsamment longues, on rduit les consquences possibles de la planication sur les rsultats. La dernire astuce employe par PutTakeTest consiste utiliser un critre de terminaison dterministe an quil ny ait pas besoin de coordination interthread supplmentaire pour savoir quand le test est ni. La mthode de test lance autant de producteurs que de consommateurs, chacun deux plaant ou retirant le mme nombre dlments an que le nombre total des lments ajouts soit gal celui des lments retirs. Les tests de PutTakeTest se rvlent assez efcaces pour trouver les violations de la thread safety. Une erreur classique lorsque lon implmente des tampons contrls par des smaphores, par exemple, est doublier que le code qui effectue linsertion et lextraction doit sexcuter en exclusion mutuelle ( laide de synchronized ou de ReentrantLock). Ainsi, le lancement de PutTakeTest avec une version de BoundedBuffer ne prcisant pas que doInsert() et doExtract() sont synchronized chouerait assez rapidement. Lancer PutTakeTest avec quelques dizaines de threads qui itrent quelques millions de fois sur des tampons de capacits varies avec des systmes diffrents augmentera votre conance quant labsence de corruption des donnes avec put() et take().
Pour augmenter la diversit des entrelacements possibles, les tests devraient seffectuer sur des systmes multiprocesseurs. Cependant, les tests ne seront pas ncessairement plus efcaces au-del de quelques processeurs. Pour maximiser la chance de dtecter les situations de comptition lies au timing, il devrait y avoir plus de threads actifs que de processeurs, an qu un instant donn certains threads sexcutent et dautres soient sortis du processeur, ce qui rduit ainsi la possibilit de prvoir les interactions entre les threads.

Avec les tests qui excutent un nombre x doprations, il est possible que le cas de test ne se termine jamais si le code test rencontre une exception cause dun bogue. La mthode la plus classique de traiter cette situation consiste faire en sorte que le framework de test mette brutalement n aux tests qui ne se terminent pas au bout dun certain temps ; ce dlai devrait tre dtermin empiriquement et les erreurs doivent tre

Chapitre 12

Tests des programmes concurrents

263

analyses pour vrier que le problme nest pas simplement d au fait que lon na pas attendu assez longtemps (ce nest pas un problme propre aux tests des classes concurrentes car les tests squentiels doivent galement distinguer les boucles longues des boucles sans n). 12.1.4 Test de la gestion des ressources Pour linstant, les tests se sont intresss la conformit dune classe par rapport sa spcication ce quelle fasse ce quelle est cense faire. Un deuxime point vrier est quelle ne fasse pas ce quelle nest pas cense faire, comme provoquer une fuite des ressources. Tout objet qui contient ou gre dautres objets ne devrait pas continuer conserver des rfrences ces objets plus longtemps quil nest ncessaire. Ces fuites de stockage empchent en effet les ramasse-miettes de rcuprer la mmoire (ou les threads, les descripteurs de chiers, les sockets, les connexions aux bases de donnes ou toute autre ressource limite) et peuvent provoquer un puisement des ressources et un chec de lapplication. Les problmes de gestion des ressources ont une importance toute particulire pour les classes comme BoundedBuffer la seule raison de borner un tampon est dempcher que lapplication nchoue cause dun puisement des ressources lorsque les producteurs sont trop en avance par rapport aux consommateurs. La limitation de la taille du tampon fera que les producteurs trop productifs se bloqueront au lieu de continuer crer du travail qui consommera de plus en plus de mmoire ou dautres ressources. On peut facilement tester la rtention abusive de la mmoire laide doutils dinspection du tas, qui permettent de mesurer lutilisation mmoire de lapplication ; il existe un grand nombre doutils commerciaux et open-source qui permettent deffectuer ce type danalyse. La mthode testLeak() du Listing 12.7 contient des emplacements pour quun tel outil puisse faire un instantan du tas, forcer lapplication du ramasse-miettes 1, puis enregistrer les informations sur la taille du tas et lutilisation de la mmoire.
Listing 12.7 : Test des fuites de ressources.
class Big { double[] data = new double[100000]; } void testLeak() throws InterruptedException { BoundedBuffer<Big> bb = new BoundedBuffer<Big>(CAPACITY); int heapSize1 = /* instantan du tas */; for (int i = 0; i < CAPACITY; i++) bb.put(new Big()); for (int i = 0; i < CAPACITY; i++) bb.take(); int heapSize2 = /* instantan du tas */; assertTrue(Math.abs(heapSize1-heapSize2) < THRESHOLD); }

1. Techniquement, il est impossible de forcer lapplication du ramasse-miettes ; System.gc() ne fait que suggrer la JVM quil pourrait tre souhaitable de le faire. On peut demander HotSpot dignorer les appels System.gc() laide de loption -XX:+DisableExplicitGC.

264

Vivacit, performances et tests

Partie III

La mthode testLeak() insre plusieurs gros objets dans un tampon born puis les en extrait ; lutilisation de la mmoire au moment du deuxime instantan du tas devrait tre approximativement identique celle releve lors du premier. Si doExtract(), en revanche, a oubli de mettre null la rfrence vers llment renvoy (en faisant items[i]=null), lutilisation de la mmoire au moment des deux instantans ne sera plus du tout la mme (cest lune des rares fois o il est ncessaire daffecter explicitement null une rfrence ; la plupart du temps, cela ne sert rien et peut mme poser des problmes [EJ Item 5]). 12.1.5 Utilisation des fonctions de rappel Les fonctions de rappel vers du code fourni par le client facilitent lcriture des cas de tests ; elles sont souvent places des points prcis du cycle de vie dun objet o elles constituent une bonne opportunit de vrier les invariants. ThreadPoolExecutor, par exemple, fait des appels des tches Runnable et ThreadFactory. Tester un pool de threads implique de tester un certain nombre dlments de sa politique dexcution : que les threads supplmentaires sont crs quand ils doivent ltre, mais pas lorsquil ne le faut pas ; que les threads inactifs sont supprims lorsquils doivent ltre, etc. La construction dune suite de tests complte qui couvre toutes les possibilits reprsente un gros travail, mais la plupart dentre elles peuvent tre testes sparment assez simplement. Nous pouvons mettre en place la cration des threads laide dune fabrique de threads personnalise. La classe TestingThreadFactory du Listing 12.8, par exemple, gre un compteur des threads crs an que les cas de tests puissent vrier ce nombre au cours de leur excution. Cette classe pourrait tre tendue pour renvoyer un Thread personnalis qui enregistre galement sa n, an que les cas de tests puissent contrler que les threads sont bien supprims conformment la politique dexcution.
Listing 12.8 : Fabrique de threads pour tester ThreadPoolExecutor.
class TestingThreadFactory implements ThreadFactory { public final AtomicInteger numCreated = new AtomicInteger(); private final ThreadFactory factory = Executors.defaultThreadFactory(); public Thread newThread(Runnable r) { numCreated.incrementAndGet(); return factory.newThread(r); } }

Si la taille du pool est infrieure la taille maximale, celui-ci devrait grandir mesure que les besoins dexcution augmentent. Soumettre des tches longues rend constant le nombre de tches qui sexcutent pendant sufsamment longtemps pour faire quelques tests. Dans le Listing 12.9, on vrie que le pool stend correctement.

Chapitre 12

Tests des programmes concurrents

265

Listing 12.9 : Mthode de test pour vrifier lexpansion du pool de threads.


public void testPoolExpansion() throws InterruptedException { int MAX_SIZE = 10; ExecutorService exec = Executors.newFixedThreadPool(MAX_SIZE, threadFactory); for (int i = 0; i < 10 * MAX_SIZE; i++) exec.execute(new Runnable() { public void run() { try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); for (int i = 0; i < 20 && threadFactory.numCreated.get() < MAX_SIZE; i++) Thread.sleep(100); assertEquals(threadFactory.numCreated.get(), MAX_SIZE); exec.shutdownNow(); }

12.1.6 Production dentrelacements supplmentaires La plupart des erreurs potentielles des codes concurrents tant des vnements peu probables, leurs tests sont un jeu de hasard, bien que lon puisse amliorer leurs chances laide de quelques techniques. Nous avons dj mentionn comment lutilisation de systmes ayant moins de processeurs que de threads actifs permettait de produire plus dentrelacement quun systme monoprocesseur ou un systme disposant de beaucoup de processeurs. De mme, effectuer des tests sur plusieurs types de systmes avec des nombres de processeurs, des systmes dexploitation diffrents et des architectures diffrentes permet de dcouvrir des problmes qui pourraient ne pas apparatre sur tous ces systmes. Une astuce utile pour augmenter le nombre dentrelacements (et donc pour explorer efcacement lespace dtat des programmes) consiste utiliser Thread.yield() pour encourager les changements de contexte au cours des oprations qui accdent ltat partag (lefcacit de cette technique dpend de la plate-forme car la JVM peut trs bien traiter Thread.yield() comme une "non-opration" [JLS 17.9] ; lemploi dun sleep() court mais non nul serait plus lent mais plus able). La mthode prsente dans le Listing 12.10 transfre des fonds dun compte vers un autre ; les invariants comme "la somme de tous les comptes doit tre gale zro" ne sont pas vris entre les deux oprations de modication. En appelant yield() au milieu dune opration, on peut parfois dclencher des bogues sensibles au timing dans du code qui ne synchronise pas correctement ses accs ltat partag. Linconvnient de lajout de ces appels pour les tests et de leur suppression pour la version de production peut tre attnu en utilisant des outils de "programmation oriente aspects".

266

Vivacit, performances et tests

Partie III

Listing 12.10 : Utilisation de Thread.yield() pour produire plus dentrelacements


public synchronized void transferCredits(Account from, Account to, int amount) { from.setBalance(from.getBalance() - amount); if (random.nextInt(1000) > THRESHOLD) Thread.yield(); to.setBalance(to.getBalance() + amount); }

12.2

Tests de performances

Les tests de performances sont souvent des versions amliores des tests fonctionnels et il est presque toujours intressant dinclure quelques tests fonctionnels de base dans les tests de performances an de sassurer que lon ne teste pas les performances dun code incorrect. Bien que les tests de performances et les tests fonctionnels se recouvrent, leurs buts sont diffrents. Les premiers tentent de mesurer les performances de bout en bout pour des cas dutilisation reprsentatifs. Il nest pas toujours ais de choisir un jeu de scnarii dutilisation raisonnable ; dans lidal, les tests devraient reter la faon dont les objets tests sont rellement utiliss dans lapplication. Dans certains cas, un scnario de test appropri simpose clairement ; les tampons borns tant presque toujours utiliss dans des conceptions producteur-consommateur. Par exemple, il est raisonnable de mesurer le dbut des producteurs qui fournissent les donnes aux consommateurs. Nous pouvons aisment tendre PutTakeTest pour en faire un test de performances pour ce scnario. Un second but classique des tests de performances est de choisir empiriquement des valeurs pour diffrentes mesures le nombre de threads, la capacit dun tampon, etc. Bien que ces valeurs puissent dpendre des caractristiques de la plate-forme (le type, voire la version du processeur, le nombre de processeurs ou la taille de la mmoire) et donc ncessiter une conguration en consquence, des choix raisonnables pour ces valeurs fonctionnent souvent correctement sur un grand nombre de systmes. 12.2.1 Extension de PutTakeTest pour ajouter un timing Lextension principale PutTakeTest concerne la mesure du temps dexcution dun test. Au lieu dessayer de le mesurer pour une seule opration, nous obtiendrons une valeur plus prcise en calculant le temps du test complet et en le divisant par le nombre doprations an dobtenir un temps par opration. Nous avons dj utilis un objet CyclicBarrier pour lancer et stopper les threads, aussi pouvons-nous continuer et utiliser une action de barrire pour noter les temps de dbut et de n, comme dans le Listing 12.11.

Chapitre 12

Tests des programmes concurrents

267

Nous modions linitialisation de la barrire pour quelle utilise cette action en nous servant du constructeur de CyclicBarrier, qui prend en paramtre une action de barrire.
Listing 12.11 : Mesure du temps laide dune barrire.
this.timer = new BarrierTimer(); this.barrier = new CyclicBarrier(npairs * 2 + 1, timer); public class BarrierTimer implements Runnable { private boolean started; private long startTime, endTime; public synchronized void run() { long t = System.nanoTime(); if (!started) { started = true; startTime = t; } else endTime = t; } public synchronized void clear() { started = false; } public synchronized long getTime() { return endTime - startTime; } }

La mthode de test modie qui mesure le temps laide de la barrire est prsente dans le Listing 12.12.
Listing 12.12 : Test avec mesure du temps laide dune barrire.
public void test() { try { timer.clear(); for (int i = 0; i < nPairs; i++) { pool.execute(new Producer()); pool.execute(new Consumer()); } barrier.await(); barrier.await(); long nsPerItem = timer.getTime() / (nPairs * (long)nTrials); System.out.print("Throughput: " + nsPerItem + " ns/item"); assertEquals(putSum.get(), takeSum.get()); } catch (Exception e) { throw new RuntimeException(e); } }

Nous pouvons apprendre plusieurs choses de lexcution de TimedPutTakeTest. La premire concerne le dbit de lopration de passage du producteur au consommateur pour les diffrentes combinaisons de paramtres ; la deuxime est ladaptation du tampon born aux diffrents nombres de threads ; la troisime est la faon dont nous pourrions choisir la taille du tampon. Rpondre ces questions ncessitant de lancer le test avec diffrentes combinaisons de paramtres, nous avons donc besoin dun pilote de test comme celui du Listing 12.13.

268

Vivacit, performances et tests

Partie III

Listing 12.13 : Programme pilote pour TimedPutTakeTest.


public static void main(String[] args) throws Exception { int tpt = 100000; // essais par thread for (int cap = 1; cap <= 1000; cap *= 10) { System.out.println("Capacity: " + cap); for (int pairs = 1; pairs <= 128; pairs *= 2) { TimedPutTakeTest t = new TimedPutTakeTest (cap, pairs, tpt); System.out.print("Pairs: " + pairs + "\t"); t.test(); System.out.print("\t"); Thread.sleep(1000); t.test(); System.out.println(); Thread.sleep(1000); } } pool.shutdown(); }

La Figure 12.1 montre les rsultats sur une machine quatre processeurs, avec des capacits de tampon de 1, 10, 100 et 1 000. On constate immdiatement quon obtient un dbit trs faible avec un tampon un seul lment car chaque thread ne peut progresser que trs lgrement avant de se bloquer et dattendre un autre thread. Laugmentation de la taille du tampon 10 amliore normment le dbit, alors que les capacits suprieures produisent des rsultats qui vont en diminuant.
Figure 12.1
10

TimedPutTakeTest
avec diffrentes capacits de tampon.
Dbit (normalis) 8 Taille=1000

6 Taille=10 4 Taille=100

2 Taille=1 0 1 2 4 8 16 32 64 128 Nombre de threads

Il peut sembler tonnant quajouter beaucoup plus de threads ne dgrade que lgrement les performances. La raison de ce phnomne est difcile comprendre uniquement partir des donnes, mais bien plus comprhensible lorsque lon utilise un outil comme perfbar pour mesurer les performances des processeurs pendant lexcution du test : mme avec de nombreux threads, il ny a pas beaucoup de calcul et la plupart du temps est pass bloquer et dbloquer les threads. Il y a donc beaucoup de processeurs inactifs pour plus de threads qui font la mme chose sans pnaliser normment les performances.

Chapitre 12

Tests des programmes concurrents

269

Cependant, ces rsultats ne devraient pas vous amener conclure que lon peut toujours ajouter plus de threads un programme producteur-consommateur utilisant un tampon born. Ce test est assez articiel dans sa simulation de lapplication : les producteurs ne font quasiment rien pour produire llment plac dans la le et les consommateurs nen font presque rien non plus. Dans une vritable application, les threads effectueraient des traitements plus complexes pour produire et consommer ces lments et cette inactivit disparatrait, ce qui aurait un impact non ngligeable sur les rsultats. Lintrt principal de ce test est de mesurer les contraintes que le passage producteur-consommateur via le tampon born impose au dbit global. 12.2.2 Comparaison de plusieurs algorithmes Bien que BoundedBuffer soit une implmentation assez solide dont les performances sont relativement correctes, il ny a aucune comparaison avecArrayBlockingQueue ou LinkedBlockingQueue (ce qui explique pourquoi cet algorithme de tampon na pas t choisi pour les classes de la bibliothque). Les algorithmes de java.util.concurrent ont t choisis et adapts en partie laide de tests comme ceux que nous dcrivons pour tre aussi efcaces que possible tout en offrant un grand nombre de fonctionnalits 1. La principale raison pour laquelle les performances de BoundedBuffer ne sont pas satisfaisantes est que put() et take() comprennent des oprations qui peuvent impliquer une comptition prendre un smaphore ou un verrou, librer un smaphore. Les autres approches contiennent moins de points susceptibles de donner lieu une comptition avec un autre thread. La Figure 12.2 compare les dbits de trois classes utilisant des tampons de 256 lments en utilisant une variante de TimedPutTakeTest. Les rsultats semblent suggrer que LinkedBlockingQueue sadapte mieux que ArrayBlockingQueue. Ceci peut sembler curieux au premier abord : une liste chane doit allouer un objet nud pour chaque insertion et semble donc devoir faire plus de travail quune le reposant sur un tableau. Cependant, bien quil y ait plus dallocations et de cots dus au ramasse-miettes, une liste chane autorise plus daccs concurrents par puts() et take() quune le reposant sur un tableau car les algorithmes de la liste chane permettent de modier simultanment sa tte et sa queue. Lallocation tant gnralement locale au thread, les algorithmes qui permettent de rduire la comptition en faisant plus dallocation sadaptent gnralement mieux (cest un autre cas o lintuition concernant les performances va lencontre des besoins de ladaptabilit).

1. Vous devriez pouvoir faire mieux si vous tes un expert en concurrence et que vous soyez prt abandonner quelques-unes des fonctionnalits offertes.

270

Vivacit, performances et tests

Partie III

Figure 12.2
Comparaison des implmentations des les bloquantes.
Dbit (normalis) 4

3 LinkedBlockingQueue 2 ArrayBlockingQueue 1 BoundedBuffer

0 1 2 4 8 16 32 64 128 Nombre de threads

12.2.3 Mesure de la ractivit Pour linstant, nous nous sommes intresss la mesure du dbit, car cest gnralement le critre de performance le plus important pour les programmes concurrents. Parfois, cependant, on prfre connatre le temps que mettra une action particulire sexcuter et, dans ce cas, nous voulons mesurer la variance du temps de service. Il est parfois plus intressant dautoriser un temps de service moyen plus long sil permet dobtenir une variance plus faible ; la prvisibilit est galement une caractristique importante de la performance. Mesurer la variance permet destimer les rponses aux questions concernant la qualit du service, comme "quel est le pourcentage doprations qui russiront en 100 millisecondes ?". Les histogrammes des temps dexcution des tches sont gnralement le meilleur moyen de visualiser les carts des temps de service. Les variances sont juste un petit peu plus compliques mesurer que les moyennes car il faut mmoriser les temps dexcution de chaque tche, ainsi que le temps total. La granularit du timer pouvant tre un facteur important lors de ces mesures (une tche peut sexcuter en un temps infrieur ou trs proche du plus petit "intervalle de temps", ce qui dformera la mesure de sa dure), nous pouvons plutt valuer le temps dexcution de petits groupes doprations put() et take(), an dviter ces artefacts. La Figure 12.3 montre les temps dexcution par tche dune variante de TimedPutTake Test utilisant un tampon de 1 000 lments, dans laquelle chacune des 256 tches concurrentes ne parcourt que 1 000 lments avec des smaphores non quitables (barres grises) et quitables (barres blanches) la diffrence entre les versions quitables et non quitables des verrous et des smaphores sera explique dans la section 13.3. Les temps dexcution avec des smaphores non quitables varient de 104 8 714 millisecondes, soit dun facteur quatre-vingts. Il peut tre rduit en imposant plus dquit dans le contrle de la concurrence, ce qui est assez facile faire dans BoundedBuffer en initialisant les smaphores en mode quitable.

Chapitre 12

Tests des programmes concurrents

271

Figure 12.3
Histogramme des temps dexcution de
200

TimedPutTakeTest
Nombre de threads

avec des smaphores par dfaut (non quitables) et des smaphores quitables.

150

100

50

Non quitable

quitable

0 1 2 4 6 8 10 38,1 Temps d'excution par thread 38,2 38,3

Comme le montre la Figure 12.3, cela rduit beaucoup la variance puisque lintervalle ne va plus que de 38 194 38 207 millisecondes mais cela diminue malheureusement normment le dbit (un test plus long avec des types de tches plus classiques montrerait srement une rduction encore plus importante). Nous avons vu prcdemment que des tailles de tampon trs petites provoquaient de nombreux changements de contexte et un mauvais dbit, mme en mode non quitable, car quasiment chaque opration impliquait un changement de contexte. Pour se rendre compte que le cot de lquit provient essentiellement du blocage des threads, nous pouvons relancer le test avec une taille de tampon de un et constater que les smaphores non quitables ont maintenant des performances comparables celles des smaphores quitables. La Figure 12.4 montre que, dans ce cas, lquit ne donne pas une moyenne beaucoup plus mauvaise ni une meilleure variance.
Figure 12.4
Histogrammes des temps dexcution pour TimedPutTakeTest avec des tampons dun seul lment.
300

Nombre de threads

200

100 Non quitable 0 150 quitable

155 160 Temps d'excution par thread

165

Par consquent, moins que les threads ne se bloquent de toute faon continuellement cause dune synchronisation trop stricte, les smaphores non quitables fournissent un bien meilleur dbit alors que les smaphores quitables produisent une variance plus

272

Vivacit, performances et tests

Partie III

faible. Ces rsultats tant si diffrents, la classe Semaphore force ses clients dcider du facteur quils souhaitent optimiser.

12.3

Piges des tests de performance

En thorie, le dveloppement des tests de performance est simple il sagit de trouver un scnario dutilisation typique, dcrire un programme qui excute plusieurs fois ce scnario et de mesurer les temps dexcution. En pratique, cependant, il faut prendre garde un certain nombre de piges de programmation qui empchent ces tests de produire des rsultats signicatifs. 12.3.1 Ramasse-miettes Le timing du ramasse-miettes tant imprvisible, il est tout fait possible quil se lance pendant lexcution dun test chronomtr. Si un programme de test effectue N itrations et ne dclenche pas le ramasse-miettes alors que litration N + 1 le dclenche, une petite variation de la taille du test peut avoir un effet important (mais fallacieux) sur le temps mesur par itration. Il existe deux stratgies pour empcher le ramasse-miettes de biaiser vos rsultats. La premire consiste sassurer quil ne se lancera jamais au cours du test (en invoquant la JVM avec loption -verbose:gc pour le vrier) ; vous pouvez aussi vous assurer que le ramasse-miettes sexcutera un certain nombre de fois au cours du test, an que le programme de test puisse tenir compte de son cot. La seconde est souvent meilleure elle ncessite un test plus long et elle rete donc souvent mieux les performances relles. La plupart des applications producteur-consommateur impliquent un certain nombre dallocations et de passage du ramasse-miettes les producteurs allouent de nouveaux objets qui sont utiliss et supprims par les consommateurs. En excutant le test du tampon born sufsamment longtemps pour attirer plusieurs fois le ramasse-miettes, on obtient des rsultats plus prcis. 12.3.2 Compilation dynamique crire et interprter les tests de performance pour des langages compils dynamiquement comme Java est bien plus difcile quavec les langages compils statiquement, comme C ou C++. La JVM Hotspot (et les autres JVM actuelles) utilise en effet une combinaison dinterprtation du pseudo-code et de compilation dynamique. La premire fois quune classe est charge, la JVM lexcute en interprtant le pseudo-code. un moment donn, si une mthode est excute sufsamment souvent, le compilateur dynamique lextrait et la traduit en code machine ; lorsque la compilation se termine, il passe de linterprtation une excution directe.

Chapitre 12

Tests des programmes concurrents

273

Le timing de la compilation est imprvisible. Vos tests ne devraient sexcuter quaprs que tout le code eut t compil : il ny a aucun intrt mesurer la vitesse du code interprt puisque la plupart des programmes sexcutent sufsamment longtemps pour que tout leur code nisse par tre compil. Autoriser le compilateur sexcuter pendant que lon mesure du temps dexcution peut donc fausser les rsultats des tests de deux faons : la compilation consomme des ressources processeur et la mesure du temps dexcution dune combinaison de code compil et interprt ne produit pas un rsultat signicatif. La Figure 12.5 montre comment ces rsultats peuvent tre fausss. Les trois lignes reprsentent lexcution du mme nombre dinstructions : la ligne A concerne une excution entirement interprte, la ligne B, une excution avec une compilation survenant en plein milieu et la ligne C, une excution o la compilation a eu lieu plus tt. On remarque donc que la compilation a une inuence importante sur le temps dexcution 1.

A B C

temps Excution interprte Compilation Excution compile

Figure12.5
Rsultats fausss par une compilation dynamique.

Le code peut galement tre dcompil (pour revenir une excution interprte) et recompil pour diffrentes raisons : pour, par exemple, charger une classe qui invalide des suppositions faites par des compilations antrieures ou an de rassembler sufsamment de donnes de prolage pour dcider quune portion de code devrait tre recompile avec des optimisations diffrentes. Un moyen dempcher la compilation de fausser les rsultats consiste excuter le programme pendant un certain temps (au moins plusieurs minutes) an que la compilation et lexcution interprte ne reprsentent quune petite fraction du temps total. On peut galement utiliser une excution "de prchauffage" dans laquelle le code est sufsamment excut pour tre compil lorsquon lance la vritable mesure. Avec Hotspot,
1. La JVM peut choisir deffectuer la compilation dans le thread de lapplication ou dans un thread en arrire-plan ; les temps dexcution seront affects de faon diffrente.

274

Vivacit, performances et tests

Partie III

loption -XX:+PrintCompilation afche un message lorsque la compilation dynamique intervient : on peut donc vrier quelle a lieu avant les mesures et non pendant. Lancer le mme test plusieurs fois dans la mme instance de la JVM permet de valider la mthodologie du test. Le premier groupe de rsultat devrait tre limin comme donnes "de prchauffage" et la prsence de rsultats incohrents dans les groupes restants signie que le test devrait tre rexamin an de comprendre pourquoi ces rsultats ne sont pas identiques. La JVM utilise diffrents threads en arrire-plan pour ses travaux de maintenance. Lorsque lon mesure plusieurs activits de calcul intense non apparentes dans le mme test, il est conseill de placer des pauses explicites entre les mesures : cela permet la JVM de combler son retard par rapport aux tches de fond avec un minimum dinterfrences de la part des tches mesures (lorsque lon value plusieurs activits apparentes plusieurs excutions du mme test, par exemple , exclure les tches en arrire-plan de la JVM peut donner des rsultats exagrment optimistes). 12.3.3 chantillonnage irraliste de portions de code Les compilateurs dynamiques utilisent des informations de prolage pour faciliter loptimisation du code compil. La JVM peut utiliser des informations spciques lexcution pour produire un meilleur code, ce qui signie que la compilation de la mthode M dans un programme peut produire un code diffrent avec un autre programme. Dans certains cas, la JVM peut effectuer des optimisations en fonction de suppositions qui ne peuvent tre que temporaires et les supprimer ensuite en invalidant le code compil si ces suppositions ne sont plus vries1. Il est donc important que les programmes de test se rapprochent de lutilisation dune application typique, mais galement quils utilisent le code approximativement comme le ferait une telle application. Sinon un compilateur dynamique pourrait effectuer des optimisations spciques un programme de test purement monothread qui ne pourraient pas sappliquer des applications relles utilisant un paralllisme au moins occasionnel. Les tests de performances multithreads ne devraient donc pas tre mlangs aux tests monothreads, mme si vous ne voulez mesurer que les performances monothreads (ce problme ne se pose pas avec TimedPutTakeTest car mme le plus petit des cas tests utilise deux threads). 12.3.4 Degrs de comptition irralistes Les applications concurrentes ont tendance entrelacer deux types de travaux trs diffrents : laccs aux donnes partages comme la rcupration de la tche suivante partir
1. La JVM peut, par exemple, utiliser une transformation pour convertir un appel de mthode virtuelle en appel direct si aucune des classes actuellement charges ne rednit cette mthode ; il invalidera ce code compil lorsquune classe rednissant la mthode sera charge par la suite.

Chapitre 12

Tests des programmes concurrents

275

dune le dattente partage et les calculs locaux aux threads lexcution de la tche en supposant quelle naccde pas elle-mme aux donnes partages. Selon les proportions relatives de ces deux types de travaux, lapplication subira des niveaux de comptition diffrents et aura des performances et une adaptabilit diffrentes. Il ny aura quasiment pas de comptition si N threads rcuprent des tches partir dune le dattente partage et les excutent, et si ces tches font des calculs intensifs qui durent longtemps (sans trop accder des donnes communes) ; le dbit sera domin par la disponibilit des ressources processeur. Si, en revanche, les tches sont trs courtes, la comptition pour la le dattente sera trs importante et le dbit sera domin par le cot de la synchronisation. Pour obtenir des rsultats ralistes, les tests de performances concurrents devraient essayer de se rapprocher du calcul local au thread effectu par une application typique tout en tudiant la coordination entre les threads ; sinon il est facile dobtenir des conclusions hasardeuses sur les endroits qui gnent la concurrence. Dans la section 11.5 nous avons vu que, pour les classes qui utilisaient des verrous (les implmentations de Map synchronises, par exemple), le fait que le verrou soit trs ou peu disput pouvait avoir des rpercussions normes sur le dbit. Les tests de cette section ne font que maltraiter lobjet Map : mme avec deux threads, toutes les tentatives dy accder donnent lieu une comptition. Cependant, si une application effectuait un grand nombre de calculs locaux aux threads chaque fois quelle accde la structure de donnes partage, le niveau de comptition pourrait tre sufsamment faible pour autoriser de bonnes performances. De ce point de vue, TimedPutTakeTest peut tre un mauvais modle pour certaines applications. Les threads ne faisant pas grand-chose, le dbit est en effet domin par le cot de leur coordination, ce qui nest pas ncessairement reprsentatif de toutes les applications qui changent des donnes entre producteurs et consommateurs via des tampons borns. 12.3.5 limination du code mort Lun des ds relever pour crire de bons programmes de tests des performances (quel que soit le langage) consiste saccommoder des compilateurs qui reprent et liminent le code mort le code qui na aucun effet sur le rsultat. Les tests ne calculant souvent rien, ils sont en effet une cible de choix pour loptimisateur. La plupart du temps, il est souhaitable quun optimisateur supprime le code mort dun programme mais cela pose problme dans le cas des tests puisque lon mesure alors moins de code que prvu. Avec un peu de chance, loptimisateur peut mme supprimer tout le programme et il sera alors vident que vos donnes sont bogues. Si vous navez pas de chance, llimination du code mort ne fera quacclrer le programme dun facteur qui pourrait sexpliquer par dautres moyens. Bien que llimination du code mort pose galement problme dans les tests de performances compils statiquement, il est bien plus facile de dtecter que le compilateur a

276

Vivacit, performances et tests

Partie III

limin une bonne partie du programme de test puisque lon peut examiner le code machine et constater quil en manque un bout. Avec les langages compils dynamiquement, cette vrication est moins facile. De nombreux micro-tests de performances se comportent "mieux" lorsquils sont excuts avec loption -server de Hotspot quavec -client : pas simplement parce que le compilateur peut alors produire du code plus efcace, mais galement parce quil optimise mieux le code mort. Malheureusement, cette optimisation qui permet de rduire le code dun test ne fera pas aussi bien avec un code qui effectue vraiment des traitements. Cependant, nous vous conseillons quand mme dutiliser -server plutt que -client sur les systmes multiprocesseurs (que ce soit pour les programmes de test ou de production) : il suft simplement dcrire les tests pour quils ne soient pas sujets llimination du code mort.
crire des tests de performances efcaces ncessite de tromper loptimisateur an quil ne considre pas le programme de test comme du code mort. Ceci implique dutiliser quelque part dans le programme tous les rsultats qui ont t calculs dune faon qui ne ncessite ni synchronisation ni calculs trop lourds.

Dans PutTakeTest, nous calculons la somme de contrle des lments ajouts et supprims de la le dattente et nous les combinons travers tous les threads : ce traitement pourrait tre supprim par loptimisateur si nous nutilisons pas vraiment cette somme de contrle. Il se trouve que nous pensions lutiliser pour vrier la justesse de lalgorithme mais, pour sassurer que nimporte quelle valeur est utilise, il suft de lafcher. Cependant, il est prfrable dviter les oprations dE/S pendant lexcution dun test, an de ne pas perturber la mesure du temps dexcution. Une astuce simple pour viter moindre cot quun calcul soit supprim par loptimisateur consiste calculer le code de hachage du champ dun objet driv, de le comparer une valeur quelconque (la valeur courante de System.nanoTime(), par exemple) et dafcher un message inutile en cas dgalit :
if (foo.x.hashCode() == System.nanoTime()) System.out.print(" ");

Cette comparaison russira rarement et, mme dans ce cas, elle naura pour effet que dinsrer un espace inoffensif dans la sortie (la mthode print() plaant ce caractre dans un tampon jusqu ce que println() soit appele, il ny aura donc pas de vritable E/S, mme si les deux valeurs sont gales). Non seulement chaque rsultat calcul doit tre utilis, mais les rsultats doivent galement ne pas tre prvisibles. Sinon un compilateur dynamique intelligent pourrait remplacer les oprations par les rsultats prcalculs. Nous avons rgl ce problme lors de la construction de PutTakeTest, mais tout programme de test dont les entres sont des donnes statiques est vulnrable ce type doptimisation.

Chapitre 12

Tests des programmes concurrents

277

12.4

Approches de tests complmentaires

Mme si nous aimerions croire quun programme de test efcace "trouvera tous les bogues", il sagit dun vu pieux. La NASA consacre aux tests plus de ressources techniques (on considre quil y a vingt testeurs pour un programmeur) que nimporte quelle socit commerciale ne pourrait se le permettre : pourtant, le code quelle produit nest pas exempt de dfauts. Pour des programmes complexes, aucun volume de test ne peut trouver toutes les erreurs. Le but des tests nest pas tant de trouver les erreurs que daugmenter lespoir que le code fonctionnera comme prvu. Comme il est irraliste de croire que lon peut trouver tous les bogues, le but des plans qualit devrait tre dobtenir la plus grande conance possible partir des tests disponibles. Comme un programme concurrent peut contenir plus de problmes quun programme squentiel, il faut donc plus de tests pour obtenir le mme niveau de conance. Pour linstant, nous nous sommes surtout intresss aux techniques permettant de construire des tests unitaires et des performances efcaces. Les tests sont essentiels pour avoir conance dans le comportement des classes concurrentes, mais ils ne constituent quune des mthodes des plans qualit que vous utilisez. Dautres mthodologies de la qualit sont plus efcaces pour trouver certains types de dfaut et moins pour en trouver dautres. En utilisant des mthodes de tests complmentaires, comme la relecture du code et lanalyse statique, vous pouvez obtenir une conance suprieure celle que vous auriez avec une approche simple, quelle quelle soit. 12.4.1 Relecture du code Aussi efcaces et importants que soient les tests unitaires et les tests de stress pour trouver les bogues de concurrence, ils ne peuvent pas se substituer une relecture rigoureuse du code par plusieurs personnes (inversement, la relecture du code ne peut pas non plus se substituer aux tests). Vous pouvez et devez concevoir des tests pour augmenter au maximum les chances de dcouvrir les erreurs de scurit vis--vis des threads et les lancer rgulirement, mais vous ne devez pas ngliger de faire relire soigneusement tout code concurrent par un autre dveloppeur que son auteur. Mme les experts de la programmation concurrente font des erreurs ; vous avez donc toujours intrt prendre le temps de faire relire votre code par quelquun dautre. Les experts en concurrence sont meilleurs que la plupart des programmes de test lorsquil sagit de dcouvrir des situations de comptition (en outre, les dtails de limplmentation de la JVM ou les modles mmoire utiliss par le processeur peuvent empcher des bogues dapparatre sur certaines congurations logicielles ou matrielles). La relecture du code a galement dautres bnces : en plus de trouver des erreurs, elle amliore souvent la qualit des commentaires qui dcrivent les dtails dimplmentation, ce qui rduit les cots et les risques dus la maintenance du code.

278

Vivacit, performances et tests

Partie III

12.4.2 Outils danalyse statiques Au moment o ce livre est crit, de nouveaux outils danalyse statiques voient le jour rgulirement pour complter les tests formels et la relecture du code. Lanalyse statique du code consiste analyser le code sans lexcuter et les outils daudit du code peuvent analyser les classes pour rechercher des types derreurs classiques dans leurs instances. Des outils comme le programme open-source FindBugs1 contiennent ainsi des dtecteurs de bogues qui savent reconnatre de nombreuses erreurs de codage classiques, dont beaucoup peuvent aisment passer travers les mailles des tests et de la relecture du code. Les outils danalyse statique produisent une liste davertissements qui doit tre examine manuellement pour dterminer sil sagit de vritables erreurs. Des utilitaires historiques comme lint produisaient tellement de faux avertissements quils effrayaient les dveloppeurs, mais les outils comme FindBugs ont t conus pour en produire moins. Bien quils soient parfois assez rudimentaires (notamment leur intgration dans les outils de dveloppement et dans le cycle de vie), leur efcacit mrite quils soient ajouts aux processus de test. Actuellement, FindBugs contient les dtecteurs suivants pour trouver les erreurs lies la concurrence, bien que dautres soient sans cesse ajouts :
m

Synchronisation incohrente. De nombreux objets utilisent une politique de synchronisation consistant protger toutes les variables par le verrou interne de lobjet. Si lon accde souvent un champ alors que lon ne dtient pas toujours ce verrou, ceci peut indiquer que lon ne respecte pas la politique de synchronisation. Les outils danalyse doivent supposer la politique de synchronisation car les classes Java ne disposent pas dun moyen de spcier formellement la concurrence. Dans le futur, si des annotations comme @GuardedBy sont standardises, ces outils pourront les interprter au lieu de deviner les relations entre les variables et les verrous, ce qui amliorera la qualit de lanalyse.

Appel de Thread.run(). Thread implmente Runnable et dispose donc dune mthode run(). Il sagit presque toujours dune erreur de lappeler directement : gnralement, le programmeur voulait appeler Thread.start(). Verrou non relch. la diffrence des verrous internes, les verrous explicites (voir Chapitre 13) ne sont pas automatiquement relchs en sortant de la porte dans laquelle ils ont t pris. Lidiome classique consiste relcher le verrou dans un bloc finally ; sinon le verrou pourrait rester verrouill en cas dexception. Bloc synchronized vide. Bien que les blocs synchronized vides aient une signication pour le modle mmoire de Java, ils sont souvent utiliss incorrectement et il existe gnralement de meilleures solutions pour rsoudre le problme.

1. Voir http://ndbugs.sourceforge.net.

Chapitre 12

Tests des programmes concurrents

279

Verrouillage contrl deux fois. Le verrouillage contrl deux fois est un idiome erron utilis pour rduire le surcot d la synchronisation dans les initialisations paresseuses (voir la section 16.2.4). Il implique de lire un champ modiable partag sans synchronisation approprie. Lancement dun thread partir dun constructeur. Lancer un thread partir dun constructeur risque de poser des problmes dhritage et peut permettre la rfrence this de schapper du constructeur. Erreurs de notication. Les mthodes notify() et notifyAll() indiquent que ltat dun objet a t modi pour dbloquer des threads qui attendent sur la le de condition approprie. Ces mthodes ne doivent tre appeles que lorsque ltat associ cette le a t modi. Un bloc synchronized qui les appelle sans modier un tat reprsente srement une erreur (voir Chapitre 14). Erreurs dattente sur une condition. Lorsque lon attend dans une le associe une condition, Object.wait() ou Condition.await() devraient toujours tre appeles dans une boucle en dtenant le verrou appropri et aprs avoir test un prdicat concernant ltat (voir Chapitre 14). Appeler ces mthodes sans dtenir de verrou en dehors dune boucle ou sans tester dtat est presque certainement une erreur. Mauvaise utilisation de Lock et Condition. Lutilisation dun objet Lock comme paramtre verrou dun bloc synchronized est probablement une erreur de frappe, tout comme appeler Condition.wait() au lieu de await() (mme si cette dernire serait probablement dtecte lors des tests puisquelle lancerait IllegalMonitorState Exception lors de son premier appel). Mise en sommeil ou en attente pendant la dtention dun verrou. Lappel de Thread.sleep() alors que lon dtient un verrou peut empcher pendant longtemps dautres threads de progresser et constitue donc un risque srieux pour la vivacit du programme. Lappel de Object.wait() ou Condition.await() avec deux verrous pose le mme type de problme. Boucles inutiles. Le code qui ne fait rien part tester si un champ a une valeur donne (attente active) peut gaspiller du temps processeur et, si ce champ nest pas volatile, nest pas certain de se terminer. Les loquets ou les attentes de conditions sont gnralement prfrables pour attendre quune transition dtat ait lieu.

12.4.3 Techniques de tests orientes aspects Actuellement, les techniques de programmation oriente aspects (POA) sont peu utilises en programmation concurrente car la plupart des outils de POA ne grent pas encore les points de synchronisation. Cependant, la POA peut servir tester les invariants ou certains aspects de la conformit aux politiques de synchronisation. (Laddad, 2003), par exemple, donne un exemple dutilisation dun aspect pour envelopper tous les appels aux mthodes non thread-safe de Swing avec lassertion que lappel a lieu dans le thread

280

Vivacit, performances et tests

Partie III

des vnements. Comme elle ne ncessite aucune modication du code, cette technique est simple appliquer et peut mettre en vidence des erreurs subtiles de publication et de connement aux threads. 12.4.4 Proleurs et outils de surveillance La plupart des outils de prolage du commerce savent grer les threads. Leurs fonctionnalits et leur efcacit varient mais ils offrent souvent une vision de ce que fait le programme (bien que les outils de prolage soient gnralement indiscrets et puissent avoir une inuence non ngligeable sur le timing et lexcution dun programme). La plupart afchent une chronologie de chaque thread avec des couleurs diffrentes pour reprsenter leurs tats (en cours dexcution, bloqu en attente dun verrou, bloqu en attente dune E/S, etc.). Cet afchage permet de mettre en vidence lutilisation des ressources processeur disponibles par le programme et, si elle est mauvaise, den constater la cause (de nombreux proleurs revendiquent galement de pouvoir identier les verrous qui provoquent les comptitions mais, en pratique, ces fonctionnalits ne sufsent pas analyser le comportement du verrouillage). Lagent JMX intgr offre galement quelques possibilits limites pour surveiller le comportement des threads. La classe ThreadInfo contient ltat du thread courant et, sil est bloqu, le verrou ou la condition quil attend. Si la fonctionnalit "surveillance de la comptition des threads" est active (elle est dsactive par dfaut cause de son impact sur les performances), ThreadInfo contient galement le nombre de fois o le thread sest bloqu en attente dun verrou ou dune notication, ainsi que la somme cumule du temps pass attendre.

Rsum
Tester la justesse des programmes concurrents peut se rvler extrmement complexe car la plupart des erreurs possibles de ces programmes sont des vnements peu frquents qui sont sensibles au timing, la charge et dautres conditions difciles reproduire. En outre, linfrastructure dun test peut introduire une synchronisation supplmentaire ou des contraintes de timing qui peuvent masquer les problmes de concurrence du code test. Tester les performances des programmes concurrents est tout aussi difcile ; les programmes Java sont plus durs tester que ceux crits dans des langages compils statiquement comme C car les mesures du temps peuvent tre affectes par la compilation dynamique, le ramasse-miettes et loptimisation adaptative. Pour avoir les meilleures chances de trouver des bogues avant quils ne surviennent en production, il faut combiner des techniques de tests traditionnelles (en vitant les piges mentionns ici) avec des relectures du code et des outils danalyse automatise. Chacune de ces techniques trouve en effet des problmes qui napparatront probablement pas aux autres.

IV
Sujets avancs

13
Verrous explicites
Avant Java 5.0, les seuls mcanismes permettant de synchroniser laccs aux donnes partages taient synchronized et volatile. Java 5.0 leur a ajout une autre option, ReentrantLock. Contrairement ce que certains ont prtendu, ReentrantLock ne remplace pas les verrous internes mais constitue plutt une alternative disposant de fonctionnalits avances lorsque ces derniers ont atteint leurs limites.

13.1

Lock et ReentrantLock

Linterface Lock, prsente dans le Listing 13.1, dnit un certain nombre doprations abstraites de verrouillage. la diffrence du verrouillage interne, Lock offre le choix entre une prise de verrou scrutable, avec dlai et interruptible ; par ailleurs, toutes les oprations de verrouillage et de dverrouillage sont explicites. Les implmentations de Lock doivent fournir la mme smantique de visibilit de la mmoire que les verrous internes mais peuvent avoir des smantiques de verrouillage, des algorithmes de planication, des garanties sur lordre des verrous et des performances diffrentes (Lock.newCondition() est prsente au Chapitre 14).
Listing 13.1 : Interface Lock.
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }

La classe ReentrantLock implmente Lock en fournissant les mmes garanties dexclusion mutuelle et de visibilit mmoire que synchronized. Prendre un ReentrantLock a la mme smantique mmoire quentrer dans un bloc synchronized et librer un ReentrantLock

284

Sujets avancs

Partie IV

a la mme smantique mmoire que sortir dun bloc synchronized (la visibilit mmoire est prsente dans la section 3.1 et au Chapitre 16). Comme synchronized, Reentrant Lock offre une smantique de verrous rentrants (voir la section 2.3.2). ReentrantLock dispose de tous les modes dacquisition des verrous dnis par Lock et permet de grer de faon plus souple que synchronized lindisponibilit des verrous. Pourquoi crer un nouveau mcanisme de verrouillage si proche des verrous internes ? Parce que ces derniers fonctionnent trs bien dans la plupart des situations mais souffrent de quelques limitations fonctionnelles il est impossible dinterrompre un thread qui attend de prendre un verrou ou de tenter de prendre un verrou sans tre prt attendre indniment. En outre, les verrous internes doivent tre relchs dans le mme bloc de code o ils ont t pris ; cela simplie le codage et saccorde bien avec le traitement des exceptions, mais cela empche aussi de crer des disciplines de verrouillage qui ne sont pas structures en blocs. Aucune de ces limitations ne constitue de raison dabandonner synchronized mais, dans certains cas, un mcanisme de verrouillage plus souple permet dobtenir une meilleure vivacit ou des performances suprieures. Le Listing 13.2 prsente lutilisation canonique dun objet Lock. Cet idiome est un peu plus compliqu que lutilisation des verrous internes puisque le verrou doit tre relch dans un bloc finally ; sinon le verrou ne serait jamais libr si le code protg levait une exception. Lorsque lon utilise des verrous, il faut aussi tenir compte de ce qui se passera si une exception survient en dehors du bloc try ; si lobjet peut rester dans un tat incohrent, il peut tre ncessaire dajouter des blocs try-catch ou try-finally supplmentaires (il faut toujours tenir compte des effets des exceptions lorsque lon utilise nimporte quelle forme de verrouillage, y compris les verrous internes).
Listing 13.2 : Protection de ltat dun objet avec ReentrantLock.
Lock lock = new ReentrantLock(); ... lock.lock(); try { // Modification de ltat de lobjet // capture les exceptions et restaure les invariants si ncessaire } finally { lock.unlock(); }

Oublier dutiliser finally pour relcher un Lock est une bombe retardement. Lorsquelle explosera, vous aurez beaucoup de mal retrouver son origine car il ny aura aucune information sur o et quand le Lock aurait d tre relch. Cest lune des raisons pour lesquelles il ne faut pas utiliser ReentrantLock comme un remplacement systmatique de synchronized : il est plus "dangereux" car il ne libre pas automatiquement le verrou lorsque le contrle quitte le bloc protg. Bien quil ne soit pas si difcile que cela de se rappeler de librer le verrou dans un bloc finally, il reste possible de loublier1.
1. FindBugs dispose dun dtecteur "verrou non libr" permettant didentier les cas o un Lock nest pas relch dans toutes les parties du code en dehors du bloc dans lequel il a t pris.

Chapitre 13

Verrous explicites

285

13.1.1 Prise de verrou scrutable et avec dlai Les modes dacquisition de verrou scrutable et avec dlai offerts par tryLock() autorisent une gestion des erreurs plus sophistique quavec une acquisition inconditionnelle. Avec les verrous internes, un interblocage est fatal la seule faon de sen sortir est de relancer lapplication et le seul moyen de sen prmunir consiste construire le programme pour empcher un ordre dacquisition incohrent. Le verrouillage scrutable et avec dlai offre une autre possibilit : lvitement probabiliste des interblocages. Lutilisation dune acquisition scrutable ou avec dlai permet de reprendre le contrle lorsque lon ne peut pas prendre tous les verrous demands, de relcher ceux qui ont dj t pris et de ressayer (ou, au moins, denregistrer lchec et de passer autre chose). Le Listing 13.3 montre un autre moyen de rsoudre linterblocage d lordre dynamique de la section 10.1.2 : il appelle tryLock() pour tenter de prendre les deux verrous mais revient en arrire et ressaie sil na pas pu prendre les deux. Pour rduire la probabilit dun livelock, le dlai dattente entre deux essais est form dune composante constante et dune autre alatoire. Si les verrous nont pas pu tre pris dans le temps imparti, transferMoney() renvoie un code derreur an que lapplication puisse chouer en douceur (voir [CPJ 2.5.1.2] et [CPJ 2.5.1.3] pour plus dexemples sur lutilisation de la prise ventuelle de verrous an dviter les interblocages).
Listing 13.3 : Utilisation de tryLock() pour viter les interblocages dus lordre des verrouillages.
public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException , InterruptedException { long fixedDelay = getFixedDelayComponentNanos(timeout, unit); long randMod = getRandomDelayModulusNanos(timeout, unit); long stopTime = System.nanoTime() + unit.toNanos(timeout); while (true) { if (fromAcct.lock.tryLock()) { try { if (toAcct.lock.tryLock()) { try { if (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException (); else { fromAcct.debit(amount); toAcct.credit(amount); return true; } } finally { toAcct.lock.unlock(); } } } finally {

286

Sujets avancs

Partie IV

Listing 13.3 : Utilisation de tryLock() pour viter les interblocages dus lordre des verrouillages. (suite)
fromAcct.lock.unlock(); } } if (System.nanoTime() > = stopTime) return false; NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); } }

Les verrous avec dlai facilitent galement limplmentation des activits qui doivent respecter des dlais (voir la section 6.3.7). Une activit dont le temps est compt appelant une mthode bloquante peut, en effet, fournir un dlai dexpiration correspondant au temps restant dans celui qui lui est allou ; cela lui permet de se terminer plus tt si elle ne peut pas dlivrer de rsultat dans le temps voulu. Avec les verrous internes, en revanche, il est impossible dannuler la prise dun verrou une fois quelle a t lance : ces verrous considrent donc les activits avec temps imparti comme des activits risque. Lexemple du portail du Listing 6.17 cre une tche distincte pour chaque socit de location de vhicule qui lui demande de passer une annonce. Cette demande dannonce ncessite srement un mcanisme rseau, comme un service web, mais peut galement exiger un accs exclusif une ressource rare, comme une ligne de communication directe vers la socit. Dans la section 9.5, nous avons dj vu comment garantir un accs srialis une ressource : il suft dutiliser un excuteur monothread. Une autre approche consiste se servir dun verrou exclusif pour protger laccs la ressource. Le code du Listing 13.4 tente denvoyer un message sur une ligne de communication protge par un Lock mais choue en douceur sil ne peut pas le faire dans le temps qui lui est imparti. Lappel temporis tryLock() permet en effet dajouter un verrou exclusif dans une activit limite par le temps.
Listing 13.4 : Verrouillage avec temps imparti.
public boolean trySendOnSharedLine(String message, long timeout, TimeUnit unit) throws InterruptedException { long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend (message); if (!lock.tryLock(nanosToLock, NANOSECONDS)) return false; try { return sendOnSharedLine(message); } finally { lock.unlock(); } }

Chapitre 13

Verrous explicites

287

13.1.2 Prise de verrou interruptible Tout comme lacquisition temporise dun verrou permet dutiliser un verrouillage exclusif avec des activits limites dans le temps, la prise de verrou interruptible permet de faire appel au verrouillage dans des activits annulables. La section 7.1.6 avait identi plusieurs mcanismes qui ne rpondent pas aux interruptions et la prise dun verrou interne en fait partie, or ces mcanismes bloquants non interruptibles compliquent limplmentation des tches annulables. La mthode lockInterruptibly() permet de tenter de prendre un verrou tout en restant ractif aux interruptions et son inclusion dans Lock vite de devoir crer une autre catgorie de mcanismes bloquants non interruptibles. La structure canonique de lacquisition interruptible dun verrou est un peu plus complique que celle dune prise de verrou classique car on a besoin de deux blocs try (si lacquisition interruptible peut lancer InterruptedException, lidiome standard tryfinally du verrouillage fonctionne). Le Listing 13.5 utilise lockInterruptibly() pour implmenter la mthode sendOnSharedLine() du Listing 13.4 an quelle puisse tre appele partir dune tche annulable. La mthode tryLock() temporise rpond galement aux interruptions et peut donc tre utilise quand on a besoin la fois dune acquisition de verrou interruptible et temporise.
Listing 13.5 : Prise de verrou interruptible.
public boolean sendOnSharedLine(String message) throws InterruptedException { lock.lockInterruptibly (); try { return cancellableSendOnSharedLine(message); } finally { lock.unlock(); } } private boolean cancellableSendOnSharedLine(String message) throws InterruptedException { ... }

13.1.3 Verrouillage non structur en bloc Avec les verrous internes, les paires prise et relchement des verrous sont structures en bloc un verrou est toujours relch dans le mme bloc que celui o il a t pris, quelle que soit la faon dont on sort du bloc. Ce verrouillage automatique simplie lanalyse du code et empche dventuelles erreurs de codage mais on a parfois besoin dune discipline plus souple. Au Chapitre 11, nous avons vu comment une granularit plus ne du verrouillage permettait damliorer ladaptabilit. Le dcoupage des verrous permet, par exemple, aux diffrentes chanes de hachage dune collection dutiliser des verrous diffrents. Nous pouvons appliquer un principe similaire pour rduire la granularit du verrouillage dans une liste chane en utilisant un verrou diffrent pour chaque nud, an que plusieurs threads puissent agir sparment sur diffrentes parties de la liste. Le verrou pour un nud

288

Sujets avancs

Partie IV

donn protge les pointeurs des liens et les donnes stockes dans ce nud ; lorsque lon parcourt ou que lon modie la liste, nous devons possder le verrou sur un nud jusqu obtenir celui du nud suivant ce nest qualors que nous pouvons relcher le verrou sur le premier nud. Un exemple de cette technique, appele verrouillage de la main la main ou couplage du verrouillage, est dcrit dans [CPJ 2.5.1.4].

13.2

Remarques sur les performances

Lorsque ReentrantLock a t ajout Java 5.0, il offrait des performances en termes de comptition bien meilleures que celles des verrous internes. Pour les primitives de synchronisation, ces performances sont la cl de ladaptabilit : plus il y a de ressources monopolises pour la gestion et la planication des verrous, moins il en reste pour lapplication. Une meilleure implmentation des verrous fait moins dappels systmes, impose moins de changements de contexte et provoque moins de trac de synchronisation sur le bus de la mmoire partage toutes ces oprations prennent du temps et dtournent du programme les ressources de calcul disponibles. Java 6 utilise un algorithme amlior pour grer les verrous internes, ressemblant celui de ReentrantLock, qui rduit considrablement lcart dadaptabilit. La Figure 13.1 montre les diffrences de performances entre les verrous internes et ReentrantLock avec Java 5.0 et sur une prversion de Java 6 sexcutant sur un systme quatre processeurs Opteron avec Solaris. Les courbes reprsentent l"acclration" de ReentrantLock par rapport aux verrous internes sur une version de la JVM. Avec Java 5.0, ReentrantLock offre un dbit considrablement meilleur alors quavec Java 6 les deux sont relativement proches 1. Le programme de test est le mme que celui de la section 11.5, mais compare cette fois-ci les dbits de HashMap protgs par un verrou interne et par un ReentrantLock.
Figure 13.1
Avantage du dbit de ReentrantLock par rapport aux verrous internes

Performances du verrouillage interne et de

5 Java 5.0

ReentrantLock avec Java 5.0 et Java 6.

2 Java 6

0 1 2 4 8 16 32 64 Nombre de threads

1. Bien que ce graphique ne le montre pas, la diffrence dadaptabilit entre Java 5.0 et Java 6 vient vraiment de lamlioration du verrouillage interne plutt que de la rgression dans ReentrantLock.

Chapitre 13

Verrous explicites

289

Avec Java 5.0, les performances des verrous internes chutent normment lorsque lon passe dun seul thread (pas de comptition) plusieurs ; celles de ReentrantLock baissent moins, ce qui montre la meilleure adaptabilit de ce type de verrou. Avec Java 6.0, cest une autre histoire les rsultats des verrous internes ne se dtriorent plus avec la comptition et les deux types de verrous ont une adaptabilit assez similaire. Des graphiques comme ceux de la Figure 13.1 nous rappellent que des afrmations comme "X est plus rapide que Y" sont, au mieux, phmres. Les performances et ladaptabilit tiennent compte de facteurs propres une plate-forme, comme le type et le nombre de processeurs, la taille du cache et les caractristiques de la JVM, qui peuvent tous voluer au cours du temps1.
Les performances sont une cible mouvante ; un test montrant hier que X tait plus rapide que Y peut ne plus tre vrai aujourdhui.

13.3

quit

Le constructeur ReentrantLock permet de choisir entre deux politiques : on peut crer un verrou non quitable (ce qui est le comportement par dfaut) ou un verrou quitable. Les threads prennent un verrou quitable dans lordre o ils lont demand alors quun verrou non quitable autorise le bousculement : les threads qui demandent un verrou peuvent sortir de la le des threads en attente si le verrou est disponible au moment o il est demand (Semaphore offre aussi le choix entre une acquisition quitable et non quitable). Les verrous ReentrantLock non quitables ne sortent pas de la le pour encourager le bousculement ils nempchent tout simplement pas un thread de passer devant tout le monde sil se prsente au bon moment. Avec un verrou quitable, un thread qui vient de le demander est mis dans la le dattente si le verrou est dtenu par un autre thread ou si dautres threads sont dj en attente du verrou ; avec un verrou non quitable, le thread nest plac dans la le dattente que si le verrou est dj dtenu par un autre thread2. Pourquoi ne pas utiliser que des verrous quitables ? Aprs tout, lquit, cest bien, et linjustice, cest mal, nest-ce pas (il suft de demander vos enfants) ? Cependant, concernant le verrouillage, lquit a un impact non ngligeable sur les performances cause des cots de suspension et de rveil des threads. En pratique, une garantie dquit statistique la promesse quun thread bloqu nira par acqurir le verrou suft
1. Quand nous avons commenc crire ce livre, ReentrantLock semblait tre le dernier cri en terme dadaptabilit des verrous. Moins de un an plus tard, les verrous internes lui ont rendu la monnaie de sa pice. Les performances ne sont pas simplement une cible mouvante, elles peuvent tre une cible trs rapide. 2. La mthode tryLock() essaie toujours de bousculer lordre, mme avec des verrous quitables.

290

Sujets avancs

Partie IV

souvent et est bien moins coteuse dlivrer. Les algorithmes qui reposent sur une le dattente quitable pour garantir leur abilit sont rares ; dans la plupart des cas, les bnces en termes de performances des verrous non quitables sont bien suprieurs ceux dune attente quitable.
Figure 13.2
Performances des verrous quitables et non quitables.
10 ConcurrentHashMap Dbit (normalis) 1 verrou non quitable 0.1

0.01 1 2 4 8 16 32 Nombre de threads

verrou quitable 64

La Figure 13.2 montre un autre test des performances de Map, mais compare cette fois-ci un HashMap envelopp avec des ReentrantLock quitables et non quitables sur un systme quatre processeurs Opteron avec Solaris ; les rsultats sont reprsents selon une chelle logarithmique1. On constate que la pnalit de lquit est denviron deux ordres de grandeur. Ne payez pas pour lquit si cela nest pas ncessaire. Une des raisons pour lesquelles les verrous non quitables ont de bien meilleures performances que les verrous quitables en cas de forte comptition est quil peut scouler un certain dlai entre le moment o un thread suspendu est rveill et celui o il sexcute vraiment. Supposons que A dtienne un verrou et que le thread B demande ce verrou. Celui-ci tant dj pris, B est suspendu. Lorsque A libre le verrou, B est rveill et peut donc tenter nouveau de le prendre. Entre-temps, si un thread C a demand le verrou, il y a de fortes chances quil lobtienne, lutilise et le libre avant mme que B ait termin de se rveiller. Dans ce cas, tout le monde est gagnant : B na pas t retard dans sa prise du verrou, C la obtenu bien plus tt et le dbit est donc amlior.

1. La courbe de ConcurrentHashMap est assez perturbe dans la rgion comprise entre quatre et huit threads. Ces variations sont presque certainement dues un bruit lors de la mesure qui pourrait avoir t produit par des interactions entre les codes de hachage des lments, la planication des threads, le changement de taille du Map, le ramasse-miettes ou dautres actions sur la mmoire, voire par le SE, qui aurait pu effectuer une tche priodique au moment de lexcution du test. La ralit est quil y a toutes sortes de variations dans les tests de performances et quil ne sert gnralement rien dessayer de les contrler. Nous navons pas voulu nettoyer articiellement le graphique car les mesures des performances dans le monde rel sont galement perturbes par le bruit.

Chapitre 13

Verrous explicites

291

Les verrous quitables fonctionnent mieux lorsquils sont dtenus pendant relativement longtemps ou que le dlai entre deux demandes de verrou est assez important. Dans ces situations, la condition qui fait que les verrous non quitables fournissent un meilleur dbit lorsque le verrou est libre mais quun thread est en train de se rveiller pour le rclamer a moins de chance dtre vrie. Comme les verrous ReentrantLock par dfaut, le verrouillage interne noffre aucune garantie dquit dterministe, mais les garanties dquit statistique de la plupart des implmentations de verrous sufsent dans quasiment toutes les situations. La spcication du langage nexige pas que la JVM implmente les verrous internes pour quils soient quitables et aucune JVM actuelle ne le fait. ReentrantLock ne rduit donc pas lquit des verrous un nouveau minimum elle rend simplement explicite quelque chose qui existait dj.

13.4

synchronized vs. ReentrantLock

ReentrantLock fournit la mme smantique de verrouillage et de mmoire que les verrous internes, ainsi que des fonctionnalits supplmentaires comme les attentes de verrous avec dlai, interruptibles, lquit et la possibilit dimplmenter un verrouillage non structur en bloc. Les performances de ReentrantLock semblent dominer celles des verrous internes, lgrement avec Java 6 mais normment avec Java 5.0. Pourquoi alors ne pas dprcier synchronized et inciter tous les nouveaux codes utiliser ReentrantLock ?

Certains auteurs ont, en ralit, fait cette proposition, en traitant synchronized comme une construction "historique" ; mais cest remiser une bonne chose bien trop loin. Les verrous internes ont des avantages non ngligeables sur les verrous explicites. Leur notation est familire et compacte et de nombreux programmes existants les utilisent mlanger les deux ne ferait quapporter de la confusion et provoquerait srement des erreurs. ReentrantLock est vraiment un outil plus dangereux que la synchronisation ; si lon oublie denvelopper lappel unlock() dans un bloc finally, le code semblera fonctionner correctement alors que lon aura cr une bombe retardement qui blessera dinnocents spectateurs. Rservez ReentrantLock pour les situations dans lesquelles vous avez besoin dune de ses fonctionnalits qui nexiste pas avec les verrous internes.

ReentrantLock est un outil avanc pour les situations dans lesquelles les verrous internes
ne sont pas appropris. Utilisez-le si vous avez besoin de ses fonctionnalits avances : pour lacquisition de verrous avec dlais, scrutable ou interruptible, pour les mises en attente quitables ou pour un verrouillage non structur en bloc. Dans les autres cas, prfrez synchronized.

292

Sujets avancs

Partie IV

Avec Java 5.0, le verrouillage interne a un autre avantage sur ReentrantLock : les traces de threads montrent quels appels ont pris quels verrous et peuvent dtecter et identier les threads en interblocage. La JVM, en revanche, ne sait rien des threads qui ont pris des ReentrantLock et ne sera donc daucun secours pour dboguer les problmes des threads qui les utilisent. Cette diffrence de traitement a t rsolue par Java 6, qui fournit une interface de gestion et de surveillance auprs de laquelle les threads peuvent senregistrer an que les informations sur les verrous externes apparaissent dans les traces et dans les autres interfaces de gestion et de dbogage. Ces informations constituent un avantage (temporaire) pour synchronized ; les informations de verrouillage dans les traces de threads ont pargn bien des soucis de nombreux programmeurs. Le fait que, par nature, ReentrantLock ne soit pas structur en bloc signie quand mme que les prises de verrous ne peuvent pas tre lies des cadres de pile spciques, comme cest le cas avec les verrous internes. Les amliorations de performances futures favoriseront srement synchronized par rapport ReentrantLock. Les verrous internes sont, en effet, intgrs la JVM, qui peut effectuer des optimisations comme llision de verrou pour les verrous conns un thread et lpaississement de verrou pour liminer la synchronisation avec les verrous internes (voir la section 11.3.2) ; faire la mme chose avec des verrous externes semble bien moins vident. moins de dployer des applications Java 5.0 dans un avenir prvisible et davoir rellement besoin des bnces de ladaptabilit fournie par Reentrant Lock sur votre plate-forme, il nest pas conseill de lutiliser la place de synchronized pour des raisons de performances.

13.5

Verrous en lecture-criture

ReentrantLock implmente un verrou dexclusion mutuelle standard : un instant donn, un ReentrantLock ne peut tre dtenu que par un et un seul thread. Cependant, lexclusion mutuelle est souvent une discipline de verrouillage plus forte quil nest ncessaire pour prserver lintgrit des donnes, et elle limite donc trop la concurrence. Il sagit en effet dune stratgie de verrouillage classique qui empche les conits entre crivain et crivain ou entre crivain et lecteur mais empche galement la coexistence dun lecteur avec un autre lecteur. Dans de nombreux cas, pourtant, les structures de donnes sont "principalement en lecture" elles sont modiables et parfois modies, mais la plupart des accs ne sont quen lecture. Dans ces situations, il serait pratique de relcher les exigences du verrouillage pour permettre plusieurs lecteurs daccder en mme temps la structure. Tant que lon est sr que chaque thread verra une version jour des donnes et quaucun autre thread ne les modiera pendant que les lecteurs les consultent, il ny aura aucun problme. Cest ce que permettent de faire les verrous en lecture-criture : plusieurs lecteurs simultanment ou un seul crivain peuvent accder une ressource, mais pas les deux.

Chapitre 13

Verrous explicites

293

Linterface ReadWriteLock, prsente dans le Listing 13.6, expose deux objets Lock un pour la lecture, lautre pour lcriture. Pour lire les donnes protges par un ReadWrite Lock il faut dabord prendre le verrou de lecture puis dtenir le verrou dcriture pour modier ces donnes. Ces deux verrous sont simplement des vues diffrentes dun objet verrou de lecture-criture.
Listing 13.6 : Interface ReadWriteLock.
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }

La stratgie de verrouillage implmente par les verrous de lecture-criture autorise plusieurs lecteurs simultans mais un seul crivain. Comme Lock, ReadWriteLock a plusieurs implmentations dont les performances, les garanties de planication, les prfrences dacquisition, lquit ou la smantique de verrouillage peuvent varier. Les verrous de lecture-criture constituent une optimisation des performances et permettent une plus grande concurrence dans certaines situations. En pratique, ils amliorent les performances pour les structures de donnes auxquelles on accde le plus souvent en lecture sur des systmes multiprocesseurs ; dans dautres situations, leurs performances sont lgrement infrieures celles des verrous exclusifs car ils sont plus complexes. Le meilleur moyen de savoir sils sont avantageux pour une application consiste donc analyser lexcution du code laide dun proleur ; ReadWriteLock utilisant Lock pour dnir les verrous de lecture et dcriture, il est relativement simple de remplacer un verrou de lecture-criture par un verrou exclusif lorsque le proleur estime quun verrou de lecture-criture napporte rien. Linteraction entre les verrous de lecture et dcriture autorise un certain nombre dimplmentations. Voici quelques-unes des options possibles pour une implmentation de ReadWriteLock :
m

Prfrence lors de la libration. Lorsquun crivain relche le verrou dcriture et que des lecteurs et des crivains sont en attente, qui doit avoir la prfrence : les lecteurs, les crivains, celui qui a demand le premier ? Priorit aux lecteurs. Si le verrou est dtenu par des lecteurs et que des crivains soient en attente, les nouveaux lecteurs doivent-ils avoir un accs immdiat ou attendre derrire les crivains ? Autoriser les lecteurs passer devant les crivains amliore ladaptabilit mais court le risque daffamer les crivains. Rentrance. Les verrous de lecture et dcriture sont-ils rentrants ? Dclassement. Si un thread dtient le verrou dcriture, peut-il prendre un verrou de lecture sans librer celui dcriture ? Cela permettrait un crivain de se "contenter" dun verrou de lecture sans laisser les autres crivains modier entre-temps la ressource protge.

m m

294

Sujets avancs

Partie IV

Promotion. Un verrou de lecture peut-il tre promu en verrou dcriture au dtriment des autres lecteurs ou crivains en attente ? La plupart des implmentations des verrous de lecture-criture nautorisent pas cette promotion car, sans opration explicite, elle risque de provoquer des interblocages (si deux lecteurs tentent simultanment de promouvoir leur verrou en verrou dcriture, aucun deux ne relchera le verrou de lecture).

ReentrantReadWriteLock fournit une smantique de verrouillage rentrant pour les deux verrous. Comme ReentrantLock, un ReentrantReadWriteLock peut tre non quitable (comportement par dfaut) ou quitable. Dans ce dernier cas, la prfrence est donne au thread qui attend le verrou depuis le plus longtemps ; si le verrou est dtenu par des lecteurs et quun thread demande le verrou dcriture, aucun autre lecteur nest autoris prendre le verrou de lecture tant que lcrivain na pas t servi et quil na pas relch le verrou dcriture. Avec un verrou non quitable, lordre daccs des threads nest pas spci. Le dclassement dun crivain en lecteur est autoris, mais pas la promotion dun lecteur en crivain (cette tentative produit un interblocage).

Comme ReentrantLock, le verrou dcriture de ReentrantReadWriteLock na quun seul propritaire et ne peut tre libr que par le thread qui la pris. Avec Java 5.0, le verrou de lecture se comporte plus comme un Semaphore que comme un verrou ; il ne mmorise que le nombre de lecteurs actifs, pas leurs identits. Ce comportement a t modi en Java 6 pour garder galement la trace des threads qui ont reu le verrou de lecture1. Le verrouillage en lecture-criture peut amliorer la concurrence lorsque les verrous sont le plus souvent dtenus pendant un temps moyennement long et que la plupart des oprations ne modient pas les ressources quils protgent. La classe ReadWriteMap du Listing 13.7 utilise ainsi un ReentrantReadWriteLock pour envelopper un Map an quil puisse tre partag sans problme par plusieurs lecteurs tout en empchant les conits lecteur-crivain et crivain-crivain2. En ralit, les performances de ConcurrentHashMap sont si bonnes que vous lutiliseriez srement la place de cette approche si vous aviez simplement besoin dun Map concurrent reposant sur un hachage ; cependant, cette technique reste utile pour fournir plus daccs concurrents une autre implmentation de Map, comme LinkedHashMap.

1. Lune des raisons de cette modication est quavec Java 5.0 limplmentation du verrou ne peut pas faire la diffrence entre un thread qui demande le verrou de lecture pour la premire fois et une demande de verrou rentrante, ce qui ferait des verrous de lecture-criture quitables des sources possibles dinterblocage. 2. ReadWriteMap nimplmente pas Map car implmenter des mthodes de consultation comme entrySet() et values() serait compliqu alors que les mthodes "faciles" sufsent gnralement.

Chapitre 13

Verrous explicites

295

Listing 13.7 : Enveloppe dun Map avec un verrou de lecture-criture.


public class ReadWriteMap<K,V> { private final Map<K,V> map; private final ReadWriteLock lock = new ReentrantReadWriteLock (); private final Lock r = lock.readLock(); private final Lock w = lock.writeLock(); public ReadWriteMap(Map<K,V> map) { this.map = map; } public V put(K key, V value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } // Idem pour remove(), putAll(), clear() public V get(Object key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } // Idem pour les autres mthodes en lecture seule de Map }

La Figure 13.3 compare les dbits dune ArrayList enveloppe avec un ReentrantLock et avec un ReadWriteLock sur un systme quatre processeurs Opteron tournant sous Solaris. Ce programme de test est comparable celui que nous avons utilis pour tester les performances de Map tout au long du livre chaque opration choisit alatoirement une valeur et la recherche dans la collection, seul un petit pourcentage dactions modie le contenu du Map.
Figure 13.3
Performances du verrouillage en lecture-criture.
Dbit (normalis) 4

3 ReentrantReadWriteLock 2

1 ReentrantLock 0 1 2 4 8 16 32 64 Nombre de threads

296

Sujets avancs

Partie IV

Rsum
Les Lock explicites disposent dun ensemble de fonctionnalits suprieur celui des verrous internes ; ils offrent notamment une plus grande souplesse dans la gestion de lindisponibilit du verrou et un plus grand contrle sur le comportement de la mise en attente. Cependant, ReentrantLock nest pas un remplaant systmatique de synchronized ; il ne faut lutiliser que lorsque lon a besoin des caractristiques absentes de synchronized. Les verrous de lecture-criture permettent plusieurs lecteurs daccder simultanment un objet protg, ce qui permet damliorer ladaptabilit lorsque lon utilise des structures de donnes qui sont plus souvent lues que modies.

14
Construction de synchronisateurs personnaliss
Les bibliothques de Java contiennent un certain nombre de classes dpendantes de ltat cest--dire des classes avec des prconditions reposant sur un tat comme FutureTask, Semaphore et BlockingQueue. On ne peut pas, par exemple, supprimer un lment dune le vide ou rcuprer le rsultat dune tche qui ne sest pas encore termine ; avant de pouvoir effectuer ces oprations, il faut attendre que la le passe dans ltat "non vide" ou que la tche entre dans ltat "termine". Le moyen le plus simple de construire une classe dpendante de ltat consiste gnralement la crer partir dune classe de la bibliothque, elle-mme dpendante de ltat : cest dailleurs ce que nous avons fait pour ValueLatch au Listing 8.17 puisque nous avions utilis la classe CountDownLatch pour fournir les blocages ncessaires. Si les classes existantes ne fournissent pas la fonctionnalit requise, il est galement possible dcrire ses propres synchronisateurs laide des mcanismes de bas niveau offerts par le langage et ses bibliothques, notamment les les dattentes de conditions internes, les objets Condition explicites et le framework AbstractQueuedSynchronizer. Ce chapitre explore les diffrents moyens de raliser cette implmentation, ainsi que les rgles qui permettent dutiliser les mcanismes fournis par la plate-forme pour grer cette dpendance.

14.1

Gestion de la dpendance par rapport ltat

Dans un programme monothread, une prcondition sur ltat (comme "le pool de connexion nest pas vide") non vrie lorsquune mthode est appele ne deviendra jamais vraie. Les classes dans les programmes squentiels peuvent donc tre crites pour chouer lorsque leurs prconditions ne sont pas vries. Dans un programme concurrent, en revanche, les conditions sur ltat peuvent voluer grce aux actions dautres threads : un pool qui tait vide il y a quelques instructions peut ne plus ltre parce quun autre thread

298

Sujets avancs

Partie IV

a renvoy un lment. Avec les objets concurrents, les mthodes qui dpendent de ltat peuvent parfois tre refuses lorsque leurs prconditions ne sont pas vries, mais il y a souvent une meilleure alternative : attendre que les prconditions deviennent vraies. Les oprations dpendantes de ltat qui se bloquent jusqu ce quelles puissent sexcuter sont plus pratiques et moins sujettes aux erreurs que celles qui se contentent dchouer. Le mcanisme interne de le dattente de condition permet aux threads de se bloquer jusqu ce quun objet passe dans un tat qui permette davancer et rveille les threads bloqus lorsquils peuvent poursuivre leur progression. Nous dcrirons les les dattente de condition dans la section 14.2 mais, pour insister sur lintrt dun mcanisme efcace dattente dune condition, nous montrerons dabord comment la dpendance par rapport ltat pourrait tre (difcilement) mise en place laide de tests et de mises en sommeil. Le Listing 14.1 prsente la structure gnrale dune action bloquante en fonction de ltat. Le motif du verrouillage est un peu inhabituel puisque le verrou est relch puis repris au milieu de lopration. Les variables dtat qui composent la prcondition doivent tre protges par le verrou de lobjet an de rester constantes pendant le test de la prcondition. Si celle-ci nest pas vrie, le verrou doit tre libr pour quun autre thread puisse modier ltat de lobjet sinon cette prcondition ne pourrait jamais devenir vraie. Le verrou doit ensuite tre repris avant de tester nouveau la prcondition.
Listing 14.1 : Structure des actions bloquantes en fonction de ltat.
prendre le verrou sur ltat de lobjet tant que (prcondition non vrifie) { librer le verrou attendre que la prcondition puisse tre vrifie chouer ventuellement en cas dinterruption ou dexpiration dun dlai reprise du verrou } effectuer laction librer le verrou

Les conceptions de type producteur-consommateur utilisent souvent des tampons borns comme ArrayBlockingQueue, qui fournit les oprations put() et take() ayant, chacune, leurs propres prconditions : on ne peut pas placer un lment dans un tampon plein ni en prendre un dans un tampon vide. Les oprations dpendantes de ltat peuvent traiter les checs des prconditions en lanant une exception ou en renvoyant un code derreur (ce qui transfre le problme lappelant), voire en se bloquant jusqu ce que lobjet passe dans ltat appropri. Nous allons dvelopper plusieurs implmentations dun tampon born en utilisant diffrentes approches pour traiter lchec des prconditions. Chacune tend la classe BaseBoundedBuffer du Listing 14.2, qui implmente un tampon circulaire laide dun tableau et dans laquelle les variables dtat (buf, head, tail et count) sont protges par le verrou interne du tampon. Cette classe fournit les mthodes synchronises doPut() et

Chapitre 14

Construction de synchronisateurs personnaliss

299

doTake(), qui seront utilises par les sous-classes pour implmenter leurs oprations put() et take() ; ltat sous-jacent est cach aux sous-classes.
Listing 14.2 : Classe de base pour les implmentations de tampons borns.
@ThreadSafe public abstract class BaseBoundedBuffer <V> { @GuardedBy("this") private final V[] buf; @GuardedBy("this") private int tail; @GuardedBy("this") private int head; @GuardedBy("this") private int count; protected BaseBoundedBuffer (int capacity) { this.buf = (V[]) new Object[capacity]; } protected synchronized final void doPut(V v) { buf[tail] = v; if (++tail == buf.length) tail = 0; ++count; } protected synchronized final V doTake() { V v = buf[head]; buf[head] = null; if (++head == buf.length) head = 0; --count; return v; } public synchronized final boolean isFull() { return count == buf.length; } public synchronized final boolean isEmpty() { return count == 0; } }

14.1.1 Exemple : propagation de lchec de la prcondition aux appelants La classe GrumpyBoundedBuffer du Listing 14.3 est une premire tentative brutale dimplmentation dun tampon born. Les mthodes put() et take() sont synchronises pour garantir un accs exclusif ltat du tampon, car elles font toutes les deux appel une logique de type tester-puis-agir lors de laccs.
Listing 14.3 : Tampon born qui se drobe lorsque les prconditions ne sont pas vrifies.
@ThreadSafe public class GrumpyBoundedBuffer <V> extends BaseBoundedBuffer <V> { public GrumpyBoundedBuffer (int size) { super(size); } public synchronized void put(V v) throws BufferFullException { if (isFull()) throw new BufferFullException(); doPut(v); }

300

Sujets avancs

Partie IV

Listing 14.3 : Tampon born qui se drobe lorsque les prconditions ne sont pas vries. (suite)
public synchronized V take() throws BufferEmptyException { if (isEmpty()) throw new BufferEmptyException(); return doTake(); } }

Bien que simple mettre en uvre, cette approche est pnible utiliser. Les exceptions sont en effet censes tre rserves aux conditions exceptionnelles [EJ Item 39] ; or "le tampon est plein" nest pas une condition exceptionnelle pour un tampon born, pas plus que "rouge" ne lest pour un feu tricolore. La simplication lors de cette implmentation (qui force lappelant grer la dpendance par rapport ltat) est plus que noye par les complications de son utilisation puisque lappelant doit tre prpar capturer les exceptions et, ventuellement, retenter chaque opration sur le tampon1. Le Listing 14.4 montre un appel take() correctement structur ce nest pas trs joli, notamment si put() et take() sont souvent appeles au cours du programme.
Listing 14.4 : Code client pour lappel de GrumpyBoundedBuffer.
while (true) { try { V item = buffer.take(); // Utilisation de item break; } catch (BufferEmptyException e) { Thread.sleep(SLEEP_GRANULARITY ); } }

Une variante de cette approche consiste renvoyer un code derreur lorsque le tampon nest pas dans le bon tat. Il sagit dune petite amlioration puisquelle nabuse plus du mcanisme des exceptions uniquement pour signier "dsol, ressayez", mais elle ne rgle pas le problme fondamental, qui est que ce sont les appelants qui doivent grer eux-mmes les checs des prconditions2. Le code client du Listing 14.4 nest pas le seul moyen dimplmenter le ressai. Lappelant pourrait retenter immdiatement take(), sans se mettre en sommeil cest ce que lon appelle une attente active. Cette approche risque de consommer beaucoup de ressources processeur si ltat du tampon ne change pas pendant un certain temps. En revanche, si lappelant dcide de se mettre en sommeil pour ne pas gaspiller trop de temps processeur, il risque de dormir trop longtemps si le tampon change dtat juste aprs lappel sleep().
1. Repousser vers lappelant la dpendance par rapport ltat empche galement la prservation de lordre FIFO ; en forant lappelant ressayer, on ne sait plus qui est arriv le premier. 2. Queue offre ces deux options poll() renvoie null si la le est vide et remove() lance une exception mais elle na pas t conue pour tre utilise dans un contexte producteur-consommateur. Lorsque des producteurs et des consommateurs doivent sexcuter en parallle, il est prfrable dutiliser BlockingQueue car ses oprations se bloquent jusqu ce que la le soit dans le bon tat.

Chapitre 14

Construction de synchronisateurs personnaliss

301

Cest donc le code du client qui doit choisir entre une mauvaise utilisation du processeur par lattente active et une mauvaise ractivit due sa mise en sommeil (un intermdiaire entre lattente active et le sommeil consiste faire appel Thread.yield() dans chaque itration pour indiquer au planicateur quil serait raisonnable de laisser un autre thread sexcuter. Si lon attend le rsultat dun autre thread, ce rsultat pourrait arriver plus vite si on libre le processeur plutt que consommer tout le quantum de temps qui nous a t imparti). 14.1.2 Exemple : blocage brutal par essai et mise en sommeil La classe SleepyBoundedBuffer du Listing 14.5 tente dpargner aux appelants linconvnient dimplmenter le code de ressai chaque appel ; pour cela, elle encapsule le mme mcanisme brutal de essayer-et-dormir lintrieur des oprations put() et take(). Si le tampon est vide, take() sendort jusqu ce quun autre thread y ajoute des donnes ; sil est plein, put() sendort jusqu ce quun autre thread fasse de la place en supprimant des donnes. Cette approche encapsule donc le traitement des prconditions et simplie lutilisation du tampon cest donc un pas dans la bonne direction.
Listing 14.5 : Tampon born avec blocage brutal.
@ThreadSafe public class SleepyBoundedBuffer <V> extends BaseBoundedBuffer <V> { public SleepyBoundedBuffer (int size) { super(size); } public void put(V v) throws InterruptedException { while (true) { synchronized (this) { if (!isFull()) { doPut(v); return; } } Thread.sleep( SLEEP_GRANULARITY ); } } public V take() throws InterruptedException { while (true) { synchronized (this) { if (!isEmpty()) return doTake(); } Thread.sleep( SLEEP_GRANULARITY ); } } }

Limplmentation de SleepyBoundedBuffer est plus complique que celle de la tentative prcdente1. Le code du tampon doit tester la condition approprie en dtenant le verrou du tampon car les variables qui reprsentent cette condition sont protges par
1. Nous passerons sous silence les cinq autres implmentations de tampons borns de Snow White, notamment SneezyBoundedBuffer.

302

Sujets avancs

Partie IV

ce verrou. Si le test choue, le thread en cours sendort pendant un moment en relchant dabord le verrou an que dautres threads puissent accder au tampon1. Lorsque le thread se rveille, il reprend le verrou et ressaie en alternant sommeil et test de la condition jusqu ce que lopration puisse se drouler. Du point de vue de lappelant, tout marche merveille lopration sexcute si elle peut avoir lieu immdiatement ou se bloque sinon et il na pas besoin de soccuper de la mcanique des checs et des ressais. Choisir la granularit du sommeil rsulte dun compromis entre ractivit et utilisation du processeur ; plus elle est ne, plus on est ractif mais plus on consomme de ressources processeur. La Figure 14.1 montre linuence de cette granularit sur la ractivit : il peut scouler un dlai entre le moment o de lespace se libre dans le tampon et celui o le thread se rveille et teste nouveau la condition.
condition U non vrifie L mise en sommeil met la condition vrai U condition vrifie

A B Figure 14.1

Sommeil trop profond car la condition devient vraie juste aprs lendormissement du thread.

SleepyBoundedBuffer impose aussi une autre exigence pour lappelant celui-ci doit traiter InterruptedException. Lorsquune mthode se bloque en attendant quune condition devienne vraie, le comportement adquat consiste fournir un mcanisme dannulation (voir Chapitre 7). Pour respecter cette rgle, SleepyBoundedBuffer gre les annulations grce une interruption en se terminant prcocement et en levant InterruptedException.

Ces tentatives de synthtiser une opration bloquante partir dessais et de mises en sommeil sont, en ralit, assez lourdes. Il serait prfrable de disposer dun moyen de suspendre un thread tout en garantissant quil sera vite rveill lorsquune certaine condition (quand le tampon nest plus plein, par exemple) devient vraie. Cest exactement ce que font les les dattentes de conditions.

1. Il est gnralement fortement dconseill quun thread sendorme ou se bloque en dtenant un verrou mais, ici, cest encore pire puisque la condition voulue (tampon plein ou vide) ne pourra jamais devenir vraie si le verrou nest pas relch !

Chapitre 14

Construction de synchronisateurs personnaliss

303

14.1.3 Les les dattente de condition Les les dattente de condition ressemblent au signal dun grille-pain indiquant que le toast est prt. Si on y est attentif, on est rapidement prvenu que lon peut faire sa tartine et abandonner ce que lon tait en train de faire (mais on peut galement vouloir nir de lire son journal) pour aller chercher le toast. Si on ne lcoute pas (parce quon est sorti chercher le journal, par exemple), on peut observer au retour dans la cuisine ltat du grillepain et rcuprer le toast sil est prt ou recommencer attendre le signal sil ne lest pas. Une le dattente de condition tire son nom du fait quelle permet un groupe de threads appel ensemble en attente dattendre quune condition spcique devienne vraie. la diffrence des les dattente classiques, dans lesquelles les lment sont des donnes, les lments dune le dattente de condition sont les threads qui attendent que la condition soit vrie. Tout comme chaque objet Java peut agir comme un verrou, tout objet peut galement se comporter comme une le dattente de condition dont lAPI est dnie par les mthodes wait(), notify() et notifyAll() de la classe Object. Le verrou et la le dattente de condition internes dun objet sont lis : pour appeler lune des mthodes de la le sur un objet X, il faut dtenir le verrou sur X. En effet, le mcanisme pour attendre des conditions reposant sur ltat est ncessairement troitement li celui qui prserve la cohrence de cet tat : on ne peut pas attendre une condition si lon ne peut pas examiner ltat et on ne peut pas librer un autre thread de son attente dune condition sans modier ltat.
Object.wait() libre le verrou de faon atomique et demande au SE de suspendre le thread courant an de permettre aux autres threads de le prendre et donc de modier ltat de lobjet. Lorsquil est rveill, le thread reprend le verrou avant de continuer. Intuitivement, appeler wait() signie "je vais dormir, mais rveille-moi quand il se passe quelque chose dintressant" et appeler lune des mthodes de notication signie "il sest pass quelque chose dintressant".

La classe BoundedBuffer du Listing 14.6 implmente un tampon born laide de wait() et notifyAll(). Son code est la fois plus simple que les versions prcdentes, plus efcace (le thread se rveille moins souvent si ltat du tampon nest pas modi) et plus ractif (le thread se rveille plus vite en cas de modication de ltat qui le concerne). Cest une grosse amlioration, mais vous remarquerez que lintroduction des les dattente de condition na pas modi la smantique par rapport aux versions prcdentes. Il sagit simplement dune optimisation plusieurs dimensions : efcacit processeur, cot des changements de contexte et ractivit. Les les dattente de condition ne permettent rien de plus que la mise en sommeil avec ressai1, mais elles facilitent beaucoup lexpression et la gestion de la dpendance par rapport ltat tout en la rendant plus efcace.
1. Ce nest pas tout fait vrai ; une le dattente de condition quitable peut garantir lordre relatif dans lequel les threads seront sortis de lensemble en attente. Les les dattente de condition internes, comme les verrous internes, ne permettent pas cette quit : les Condition explicites offrent ce choix.

304

Sujets avancs

Partie IV

Listing 14.6 : Tampon born utilisant des files dattente de condition.


@ThreadSafe public class BoundedBuffer<V> extends BaseBoundedBuffer <V> { // PRDICAT DE CONDITION : non-plein (!isFull()) // PRDICAT DE CONDITION : non-vide (!isEmpty()) public BoundedBuffer(int size) { super(size); } // BLOQUE JUSQU : non-plein public synchronized void put(V v) throws InterruptedException { while (isFull()) wait(); doPut(v); notifyAll(); } // BLOQUE JUSQU : non-vide public synchronized V take() throws InterruptedException { while (isEmpty()) wait(); V v = doTake(); notifyAll(); return v; } }

La classe BoundedBuffer est nalement assez bonne pour tre utilise elle est simple demploi et gre correctement la dpendance par rapport ltat 1. Une version de production devrait galement inclure une version avec dlai de put() et take() an que les oprations bloquantes puissent expirer si elles narrivent pas se terminer dans le temps imparti. La version avec dlai de Object.wait() permet dimplmenter facilement cette fonctionnalit.

14.2

Utilisation des les dattente de condition

Les les dattente de condition facilitent la construction de classes dpendantes de ltat efcaces et ractives mais on peut quand mme les utiliser incorrectement ; beaucoup de rgles sur leur bon emploi ne sont pas imposes par le compilateur ou la plate-forme (cest lune des raisons de sappuyer sur des classes comme LinkedBlockingQueue, CountDownLatch, Semaphore et FutureTask lorsque cela est possible, car cest bien plus simple). 14.2.1 Le prdicat de condition La cl pour utiliser correctement les les dattente de condition est didentier les prdicats de condition que lobjet peut attendre. Ce sont ces prdicats qui causent la plupart des confusions propos de wait() et notify() car il ny a rien dans lAPI ni dans la spcication du langage ou limplmentation de la JVM qui garantisse leur
1. La classe ConditionBoundedBuffer de la section 14.3 est encore meilleure : elle est plus efcace car elle peut utiliser une seule notication au lieu de notifyAll().

Chapitre 14

Construction de synchronisateurs personnaliss

305

emploi correct. En fait, ils ne sont jamais mentionns directement dans la spcication du langage ou dans Javadoc. Pourtant, sans eux, les attentes de condition ne pourraient pas fonctionner. Un prdicat de condition est la prcondition qui rend une opration dpendante de ltat. Dans un tampon born, take() ne peut se drouler que si le tampon nest pas vide ; sinon elle doit attendre. Pour cette opration, le prdicat de condition est donc "le tampon nest pas vide" et take() doit le tester avant de faire quoi que ce soit. De mme, le prdicat de condition de put() est "le tampon nest pas plein". Les prdicats de condition sont des expressions construites partir des variables dtat de la classe ; BaseBounded Buffer teste que "le tampon nest pas vide" en comparant count avec zro et que "le tampon nest pas plein" en comparant count avec la taille du tampon.
Documentez le ou les prdicats de condition associs une le dattente de condition et les oprations qui attendent quils soient vris.

Une attente de condition contient une relation tripartite importante qui implique le verrouillage, la mthode wait() et un prdicat de condition. Ce dernier implique les variables dtat, qui sont protges par un verrou ; avant de tester le prdicat de condition, il faut donc dtenir ce verrou, qui doit tre le mme que lobjet le dattente de condition (celui sur lequel wait() et notify() sont invoques). Dans BoundedBuffer, ltat du tampon est protg par le verrou du tampon et lobjet tampon est utilis comme le dattente de condition. La mthode take() prend le verrou du tampon, puis teste le prdicat de condition ("le tampon nest pas vide"). Si le tampon nest pas vide, la mthode supprime le premier lment, ce quelle peut faire puisquelle dtient toujours le verrou qui protge ltat du tampon. Si le prdicat de condition nest pas vri (le tampon est vide), take() doit attendre quun autre thread y place un objet. Pour cela, elle appelle wait() sur la le dattente de condition interne du tampon, ce qui ncessite de dtenir le verrou sur cet objet le. Avec une conception soigneuse, take() dtient dj le verrou, qui lui est ncessaire pour tester le prdicat de condition (et, si ce dernier est vri, pour modier ltat du tampon dans la mme opration atomique). La mthode wait() libre le verrou, bloque le thread courant et attend jusqu ce que le dlai imparti se soit coul, que le thread soit interrompu ou quil soit rveill par une notication. Aprs le rveil du thread, wait() reprend le verrou avant de se terminer. Un thread qui se rveille de wait() na aucune priorit particulire sur la reprise du verrou ; il lutte pour son acquisition au mme titre que nimporte quel autre thread dsirant entrer dans un bloc synchronized. Chaque appel wait() est implicitement associ un prdicat de condition spcique. Lorsquil appelle wait() par rapport un prdicat particulier, lappelant doit dj dtenir

306

Sujets avancs

Partie IV

le verrou associ la le dattente de condition et ce verrou doit galement protger les variables dtat qui composent le prdicat de la condition. 14.2.2 Rveil trop prcoce Comme si la relation triangulaire entre le verrou, le prdicat de condition et la le dattente de condition ntait pas assez complique, le fait que wait() se termine ne signie pas ncessairement que le prdicat de condition quattend le thread soit devenu vrai. Une mme le dattente de condition interne peut tre utilise avec plusieurs prdicats de condition. Lorsque le thread est rveill parce que quelquun a appel notifyAll(), cela ne signie pas ncessairement que le prdicat attendu soit dsormais vrai (cest comme si le grille-pain et la machine caf partageaient la mme sonnerie ; lorsquelle retentit, vous devez aller vrier quelle machine a dclench le signal1). En outre, wait() peut mme se terminer "sauvagement" pas en rponse un thread qui appelle notify()2. Lorsque le contrle repasse nouveau dans le code qui a appel wait(), il a repris le verrou associ la le dattente de condition. Est-ce que le prdicat de condition est maintenant vri ? Peut-tre. Il a pu tre vrai au moment o le thread noticateur a appel notifyAll() mais redevenir faux pendant le temps o vous avez repris le verrou. Dautres threads ont pu prendre le verrou et modier ltat de lobjet entre le moment o votre thread sest rveill et celui o wait() a repris le verrou. Peut-tre mme na-t-il jamais t vrai depuis que vous avez appel wait(). Vous ne savez pas pourquoi un autre thread a appel notify() ou notifyAll() ; peut-tre la-t-il fait parce quun autre prdicat de condition associ la mme le dattente de condition est devenu vrai. Il est assez frquent que plusieurs prdicats soient associs la mme le BoundedBuffer, par exemple, utilise la mme le dattente de condition pour les deux prdicats "le tampon nest pas plein" et "le tampon nest pas vide"3. Pour toutes ces raisons, il faut tester nouveau le prdicat de condition lorsque lon se rveille dun appel wait() et recommencer attendre (ou chouer) sil nest pas vrai. Comme on peut se rveiller plusieurs fois sans que le prdicat ne soit vrai, il faut toujours appeler wait() dans une boucle en testant le prdicat de condition chaque itration. Le Listing 14.7 montre la forme canonique dune attente de condition.

1. Cette situation dcrit trs bien la cuisine de lauteur ; il y a tant de machines qui sonnent que, lorsque lon entend un signal, il faut vrier le grille-pain, le micro-ondes, la machine caf et bien dautres encore pour dterminer la cause de la sonnerie. 2. Pour aller encore plus loin dans lanalogie avec le petit djeuner, cest comme si le grille-pain sonnait lorsque le toast est prt mais galement parfois quand il ne lest pas. 3. En fait, il est possible que les threads attendent en mme temps "non plein" et "non vide" ! Cette situation peut arriver lorsque le nombre de producteurs-consommateurs dpasse la capacit du tampon.

Chapitre 14

Construction de synchronisateurs personnaliss

307

Listing 14.7 : Forme canonique des mthodes dpendantes de ltat.


void stateDependentMethod() throws InterruptedException { // Le prdicat de condition doit tre protg par un verrou. synchronized(lock) { while (!conditionPredicate()) lock.wait(); // Lobjet est dsormais dans ltat souhait } }

Lorsque vous attendez une condition avec Object.wait() ou Condition.await() : Ayez toujours un prdicat de condition un test de ltat de lobjet qui doit tre vri avant de continuer. Testez toujours le prdicat de condition avant dappeler wait() et nouveau aprs le retour de wait(). Appelez toujours wait() dans une boucle. Vriez que les variables dtat qui composent le prdicat de condition sont protges par le verrou associ la le dattente de condition. Prenez le verrou associ la le dattente de condition lorsque vous appelez wait(), notify() ou notifyAll(). Relchez le verrou non pas aprs avoir test le prdicat de condition, mais avant dagir sur ce prdicat.

14.2.3 Signaux manqus Le Chapitre 10 a prsent les checs de vivacit comme les interblocages (deadlocks) et les livelocks. Les signaux manqus sont une autre forme dchec de la vivacit. Un signal manqu a lieu lorsquun thread doit attendre une condition qui est dj vraie mais quil oublie de vrier le prdicat de la condition avant de se mettre en attente. Le thread attend alors une notication dun vnement qui a dj eu lieu. Cest comme lancer le grille-pain, partir chercher le journal en laissant la sonnerie retentir pendant que lon est dehors puis se rasseoir la table en attendant que le grille-pain sonne : vous pourriez attendre longtemps srement ternellement1. la diffrence de la conture sur le toast, la notication ne "colle" pas si un thread A envoie une notication une le dattente de condition et quun thread B se mette ensuite en attente sur cette le, B ne se rveillera pas immdiatement : il faudra une autre notication pour le rveiller. Les signaux manqus sont le rsultat derreurs provenant, par exemple, du non-respect des conseils de la liste ci-dessus ne pas tester le prdicat de condition avant dappeler wait(),
1. Pour sortir de cette attente, quelquun dautre devrait faire griller un toast, mais cela ne ferait quempirer le problme : lorsque la sonnerie retentirait, vous auriez alors un dsaccord sur la proprit de la tartine.

308

Sujets avancs

Partie IV

notamment. Si vous structurez vos attentes de condition comme dans le Listing 14.7, vous naurez pas de problme avec les signaux manqus. 14.2.4 Notication Pour linstant, nous navons dcrit que la moiti de ce qui se passe lors dune attente de condition : lattente. Lautre moiti est la notication. Dans un tampon born, take() se bloque si elle est appele alors que le tampon est vide. Pour que take() se dbloque quand le tampon devient non vide, on doit sassurer que chaque partie du code dans laquelle le tampon pourrait devenir non vide envoie une notication. Dans BoundedBuffer, il ny a quun seul endroit aprs un put(). Par consquent, put() appelle notifyAll() aprs avoir russi ajouter un objet dans le tampon. De mme, take() appelle notifyAll() aprs avoir supprim un lment pour indiquer que le tampon peut ne plus tre plein, au cas o des threads attendraient sur la condition "non plein".
chaque fois que lon attend sur une condition, il faut sassurer que quelquun enverra une notication lorsque le prdicat de cette condition devient vrai.

Il y a deux mthodes de notication dans lAPI des les dattente de condition notify() et notifyAll(). Pour appeler lune ou lautre, il faut dtenir le verrou associ lobjet le de condition. Lappel de notify() demande la JVM de choisir un thread parmi ceux qui attendent dans cette le et de le rveiller, tandis quun appel notifyAll() rveille tous les threads en attente dans la le. Comme il faut dtenir le verrou sur lobjet le dattente de condition lorsque lon appelle notify() ou notifyAll() et que les threads en attente ne peuvent pas revenir du wait() sans reprendre le verrou, le thread qui envoie la notication doit relcher ce verrou rapidement pour que les threads en attente soient dbloqus le plus vite possible. Plusieurs threads pouvant attendre dans la mme le dattente de condition pour des prdicats de condition diffrents, il peut tre dangereux dutiliser notify() au lieu de notifyAll(), essentiellement parce quune unique notication ouvre la voie un problme semblable aux signaux manqus.
BoundedBuffer offre une bonne illustration de la raison pour laquelle il vaut mieux utiliser notifyAll() plutt quune notication simple dans la plupart des cas. La le dattente de condition sert deux prdicats de condition diffrents : "non plein" et "non vide". Supposons que le thread A attende dans une le la prcondition PA alors que le thread B attend dans la mme le le prdicat PB. Supposons maintenant que PB devienne vrai et que le thread C envoie une notication simple : la JVM rveillera donc un thread de son choix. Si elle choisit A, celui-ci se rveillera, constatera que PA nest pas vrie et se remettra en attente. Pendant ce temps, B, qui aurait pu progresser, ne sest pas rveill. Il ne sagit donc pas exactement dun signal manqu cest plutt un "signal vol" , mais le problme est identique : un thread attend un signal qui sest dj produit (ou aurait d se produire).

Chapitre 14

Construction de synchronisateurs personnaliss

309

Les notications simples ne peuvent tre utilises la place de notifyAll() que lorsque les deux conditions suivantes sont runies : Attentes uniformes. Un seul prdicat de condition est associ la le dattente de condition et chaque thread excute le mme code aprs lappel wait(). Un seul dedans. Une notication sur la variable condition autorise au plus un seul thread continuer son excution.

BoundedBuffer satisfait lexigence un seul dedans, mais pas celle dattente uniforme car les threads en attente peuvent attendre sur la condition "non plein" et "non vide". En revanche, un loquet "porte dentre" comme celui utilis par la classe TestHarness du Listing 5.11, dans laquelle un unique vnement librait un ensemble de threads, ne satisfait pas cette exigence puisque louverture de la porte permet plusieurs threads de sexcuter.

La plupart des classes ne satisfaisant pas ces exigences, la sagesse recommande dutiliser notifyAll() plutt que notify(). Bien que cela puisse ne pas tre efcace, cela permet de vrier bien plus facilement que les classes se comportent correctement. Cette "sagesse" met certaines personnes mal laise, et pour de bonnes raisons. Lutilisation de notifyAll() quand un seul thread peut progresser nest pas efcace cette pnalit est parfois faible, parfois assez nette. Si dix threads attendent dans une le de condition, lappel de notifyAll() les forcera tous se rveiller et combattre pour lacquisition du verrou, puis la plupart dentre eux (voire tous) retourneront se mettre en sommeil. Ceci implique donc beaucoup de changements de contexte et des prises de verrou trs disputes pour chaque vnement qui autorise (ventuellement) un seul thread progresser (dans le pire des cas, lutilisation de notifyAll() produit O(n2) alors que n sufrait). Cest un autre cas o les performances demandent une certaine approche et la scurit vis--vis des threads, une autre. La notication de put() et take() dans BoundedBuffer est classique : elle est effectue chaque fois quun objet est plac ou supprim du tampon. On pourrait optimiser en observant quun thread ne peut tre libr dun wait() que si le tampon passe de vide non vide ou de plein non plein et nenvoyer de notication que si un put() ou un take() effectue lune de ces transitions dtat : cest ce que lon appelle une notication conditionnelle. Bien quelle puisse amliorer les performances, la notication conditionnelle est difcile mettre en place correctement (en outre, elle complique limplmentation des sous-classes) et doit tre utilise avec prudence. Le Listing 14.8 illustre son utilisation avec la mthode put() de BoundedBuffer.

310

Sujets avancs

Partie IV

Listing 14.8 : Utilisation dune notification conditionnelle dans BoundedBuffer.put().


public synchronized void put(V v) throws InterruptedException { while (isFull()) wait(); boolean wasEmpty = isEmpty(); doPut(v); if (wasEmpty) notifyAll(); }

Les notications simples et conditionnelles sont des optimisations. Comme toujours, respectez le principe "Faites dabord en sorte que cela fonctionne et faites ensuite que cela aille vite si cela nest pas dj assez rapide" : il est trs facile dintroduire dtranges problmes de vivacit lorsquon utilise incorrectement ce type de notication. 14.2.5 Exemple : une classe "porte dentre" Le loquet "porte dentre" de TestHarness (voir Listing 5.11) est construit laide dun compteur initialis un, ce qui cre un loquet binaire deux tats : un tat initial et un tat nal. Le loquet empche les threads de passer la porte dentre tant quelle nest pas ouverte, la suite de quoi tous les threads peuvent passer. Bien que ce mcanisme soit parfois exactement ce dont on a besoin, le fait que cette sorte de porte ne puisse pas tre referme une fois ouverte a quelquefois des inconvnients. Comme le montre le Listing 14.9, les attentes de condition permettent dcrire assez facilement une classe ThreadGate qui peut tre referme. ThreadGate autorise louverture et la fermeture de la porte en fournissant une mthode await() qui se bloque jusqu ce que la porte soit ouverte. La mthode open() utilise notifyAll() car la smantique de cette classe ne respecterait pas le test "un seul dedans" avec une notication simple.
Listing 14.9 : Porte refermable laide de wait() et notifyAll().
@ThreadSafe public class ThreadGate { // PRDICAT DE CONDITION : ouverte-depuis(n) (isOpen || generation > n) @GuardedBy("this") private boolean isOpen; @GuardedBy("this") private int generation; public synchronized void close() { isOpen = false; } public synchronized void open() { ++generation; isOpen = true; notifyAll(); } // BLOQUE-JUSQU : ouverte-depuis(generation lentre) public synchronized void await() throws InterruptedException { int arrivalGeneration = generation; while (!isOpen && arrivalGeneration == generation) wait(); } }

Chapitre 14

Construction de synchronisateurs personnaliss

311

Le prdicat de condition utilis par await() est plus compliqu quun simple test de isOpen(). En effet, si N threads attendent la porte lorsquelle est ouverte, ils doivent tous tre autoriss entrer. Or, si la porte est ouverte puis referme rapidement, tous les threads pourraient ne pas tre librs si await() examinait simplement isOpen() : au moment o ils reoivent tous la notication, reprennent le verrou et sortent de wait(), la porte peut dj stre referme. ThreadGate emploie donc un prdicat de condition plus compliqu : chaque fois que la porte est ferme, un compteur "gnration" est incrment et un thread peut passer await() si la porte est ouverte ou a t ouverte depuis quil est arriv sur la porte. Comme ThreadGate ne permet que dattendre que la porte soit ouverte, elle nenvoie une notication que dans open() ; pour supporter la fois les oprations "attente douverture" et "attente de fermeture", elle devrait produire cette notication dans open() et dans close(). Ceci illustre la raison pour laquelle les classes dpendantes de ltat peuvent tre dlicates maintenir lajout dune nouvelle opration dpendante de ltat peut ncessiter de modier de nombreuses parties du code pour que les notications appropries puissent tre envoyes. 14.2.6 Problmes de safety des sous-classes Lutilisation dune notication simple ou conditionnelle introduit des contraintes qui peuvent compliquer lhritage [CPJ 3.3.3.3]. Si lon veut pouvoir hriter, il faut en effet structurer les classes pour que les sous-classes puissent ajouter la notication approprie pour le compte de la classe de base lorsquelle est hrite dune faon qui viole lune des exigences des notications simples ou conditionnelles. Une classe dpendante de ltat doit exposer (et documenter) ses protocoles dattente et de notication aux sous-classes ou empcher celles-ci dy participer (cest une extension du principe "concevez et documentez vos classes pour lhritage ou empchez-le" [EJ Item 15]). Au minimum, la conception dune classe dpendante de ltat pour quelle puisse tre hrite exige dexposer les les dattente de condition et les verrous et de documenter les prdicats de condition et la politique de synchronisation ; il peut galement tre ncessaire dexposer les variables dtat sous-jacentes (le pire quune classe dpendante de ltat puisse faire est dexposer son tat aux sous-classes sans documenter ses invariants). Une possibilit reste effectivement dinterdire lhritage, soit en marquant la classe comme final, soit en cachant aux sous-classes ses les dattente de condition, ses verrous et ses variables dtat. Dans le cas contraire, si la sous-classe fait quelque chose qui dtriore lutilisation de notify() par la classe de base, elle doit pouvoir rparer les dgts. Considrons une pile non borne bloquante dans laquelle lopration pop() se bloque lorsque la pile est vide alors que lopration push() russit toujours : cette classe correspond aux exigences dune notication simple. Si elle utilise une notication simple et quune sous-classe ajoute une mthode bloquante "dpile deux lments conscutifs", on a maintenant deux types dattentes : ceux qui attendent de dpiler un seul

312

Sujets avancs

Partie IV

lment et ceux qui attendent den dpiler deux. Si la classe de base expose la le dattente de condition et documente les protocoles permettant de lutiliser, la sous-classe peut rednir la mthode push() pour quelle effectue un notifyAll() an de restaurer la safety. 14.2.7 Encapsulation des les dattente de condition Il est gnralement prfrable dencapsuler la le dattente de condition pour quelle ne soit pas accessible lextrieur de larborescence de classes dans laquelle elle est utilise. Sinon les appelants pourraient tre tents de penser quils comprennent vos protocoles dattente et de notication et de les utiliser de faon incohrente par rapport votre conception (il est impossible dimposer lexigence dattente uniforme pour une notication simple si la le dattente de condition est accessible un code que vous ne contrlez pas ; si un code tranger attend par erreur sur votre le dattente, cela pourrait perturber votre protocole de notication et provoquer un vol de signal). Malheureusement, ce conseil encapsuler les objets utiliss comme les dattente de condition nest pas cohrent avec la plupart des patrons de conception classiques des classes thread-safe, o le verrou interne dun objet sert protger son tat. BoundedBuffer illustre cet idiome classique, o lobjet tampon lui-mme est le verrou et la le dattente de condition. Cependant, cette classe pourrait aisment tre restructure pour utiliser un objet verrou et une le dattente de condition privs ; la seule diffrence serait quelle ne permettrait plus aucune forme de verrouillage ct client. 14.2.8 Protocoles dentre et de sortie Wellings (Wellings, 2004) caractrise lutilisation correcte de wait() et notify() en termes de protocoles dentre et de sortie. Pour chaque opration dpendante de ltat et pour chaque opration modiant ltat dont dpend une autre opration, il faudrait dnir et documenter un protocole dentre et de sortie. Le protocole dentre est le prdicat de condition de lopration, celui de sortie consiste examiner toutes les variables dtat qui ont t modies par lopration, an de savoir si elles pourraient impliquer quun autre prdicat de condition devienne vrai et, dans ce cas, notier la le dattente de condition concerne. La classe AbstractQueuedSynchronizer, sur laquelle la plupart des classes de java.util .concurrent dpendantes de ltat sont construites (voir la section 14.4), exploite ce concept de protocole de sortie. Au lieu de laisser les classes synchroniseurs effectuer leur propre notication, elle exige que les mthodes synchroniseurs renvoient une valeur indiquant si laction aurait pu dbloquer un ou plusieurs threads en attente. Cette exigence explicite de lAPI rend plus difcile loubli dune notication concernant certaines transitions dtat.

Chapitre 14

Construction de synchronisateurs personnaliss

313

14.3

Objets conditions explicites

Comme nous lavons vu au Chapitre 13, les Lock explicites peuvent tre utiles dans les situations o les verrous internes ne sont pas assez souples. Tout comme Lock est une gnralisation des verrous internes, linterface Condition (dcrite dans le Listing 14.10) est une gnralisation des les dattente de condition internes.
Listing 14.10 : Interface Condition.
public interface Condition { void await() throws InterruptedException ; boolean await(long time, TimeUnit unit) throws InterruptedException ; long awaitNanos(long nanosTimeout) throws InterruptedException ; void awaitUninterruptibly (); boolean awaitUntil(Date deadline) throws InterruptedException ; void signal(); void signalAll(); }

Ces les ont, en effet, plusieurs inconvnients. Chaque verrou interne ne peut tre associ qu une seule le dattente de condition, ce qui signie, dans des classes comme Bounded Buffer, que plusieurs threads peuvent attendre dans la mme le pour des prdicats de condition diffrents et que le patron de verrouillage le plus classique implique dexposer lobjet le dattente de condition. Ces deux facteurs empchent dimposer lexigence dattente uniforme pour utiliser notify(). Pour crer un objet concurrent avec plusieurs prdicats de condition ou pour avoir plus de contrle sur la visibilit de la le dattente de condition, il est prfrable dutiliser les classes Lock et Condition explicites, car elles offrent une alternative plus souple que leurs quivalents internes. Une Condition est associe un seul Lock, tout comme une le dattente de condition est associe un seul verrou interne ; on cre une Condition, en appelant Lock.newCondition() sur le verrou concern. Tout comme Lock offre plus de fonctionnalits que les verrous internes, Condition en fournit plus que les les dattente de condition : plusieurs attentes par verrou, attentes de condition interruptibles ou non interruptibles, attentes avec dlai et choix entre une attente quitable ou non quitable. la diffrence des les dattente de condition internes, il est possible davoir autant dobjets Condition que lon souhaite avec un mme Lock. Les objets Condition hritent leur quit du Lock auquel ils sont associs ; avec un verrou quitable, les threads sont librs de Condition.await() selon lordre FIFO.
Attention : les quivalents de wait(), notify() et notifyAll() pour les objets Condition sont, respectivement, await(), signal() et signalAll(). Cependant, Condition hrite de Object et dispose donc galement de wait() et des deux mthodes de notication. Assurez-vous par consquent dutiliser les versions correctes await() et les deux mthodes signal !

314

Sujets avancs

Partie IV

Le Listing 14.11 montre encore une autre implmentation dun tampon born, cette fois-ci laide de deux Condition, notFull et notEmpty, pour reprsenter exactement les prdicats de condition "non plein" et "non vide". Lorsque take() se bloque parce que le tampon est vide, elle attend notEmpty et put() dbloque tous les threads bloqus dans take() en envoyant un signal sur notEmpty.
Listing 14.11 : Tampon born utilisant des variables conditions explicites.
@ThreadSafe public class ConditionBoundedBuffer <T> { protected final Lock lock = new ReentrantLock (); // PRDICAT DE CONDITION: notFull (count < items.length) private final Condition notFull = lock.newCondition(); // PRDICAT DE CONDITION: notEmpty (count > 0) private final Condition notEmpty = lock.newCondition(); @GuardedBy("lock") private final T[] items = (T[]) new Object[BUFFER_SIZE]; @GuardedBy("lock") private int tail, head, count; // BLOQUE-JUSQU : notFull public void put(T x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[tail] = x; if (++tail == items.length) tail = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } // BLOQUE-JUSQU : notEmpty public T take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); T x = items[head]; items[head] = null; if (++head == items.length) head = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }

Le comportement de ConditionBoundedBuffer est identique celui de BoundedBuffer, mais lutilisation de deux les dattente de condition le rend plus lisible il est plus facile danalyser une classe qui utilise plusieurs Condition quune classe qui nutilise quune seule le dattente de condition avec plusieurs prdicats de condition. En sparant

Chapitre 14

Construction de synchronisateurs personnaliss

315

les deux prdicats sur des attentes diffrentes, Condition facilite lapplication des exigences pour les notications simples. Utiliser signal(), qui est plus efcace que signalAll(), rduit le nombre de changements de contexte et de prises de verrous dclenchs par chaque opration du tampon. Comme les verrous et les les dattente de condition internes, la relation triangulaire entre le verrou, le prdicat de condition et la variable de condition doit galement tre vrie lorsque lon utilise des Lock et des Condition explicites. Les variables impliques dans le prdicat de condition doivent tre protges par le Lock et lui-mme doit tre dtenu lorsque lon teste le prdicat de condition et que lon appelle await() et signal()1. Vous devez choisir entre les Condition explicites et les les dattente de condition internes comme vous le feriez pour ReentrantLock et synchronized : utilisez Condition si vous avez besoin de fonctionnalits supplmentaires, comme lattente quitable ou plusieurs types dattentes par verrou, mais prfrez les les internes dans les autres cas (si vous utilisez dj ReentrantLock parce que vous avez besoin de ses fonctionnalits tendues, votre choix est dj fait).

14.4

Anatomie dun synchronisateur

Les interfaces de ReentrantLock et Semaphore partagent beaucoup de points communs. Ces deux classes agissent comme des "portes" en ne laissant passer la fois quun nombre limit de threads ; ceux-ci arrivent la porte et sont autoriss la franchir (si les appels lock() ou acquire() russissent), doivent attendre (si lock() ou acquire() se bloquent) ou sont dtourns (si tryLock() ou tryAcquire() renvoient false, ce qui indique que le verrou ou le permis nest pas disponible dans le temps imparti). En outre, toutes les deux autorisent des tentatives interruptibles, non interruptibles et avec dlai dexpiration, ainsi que le choix entre une mise en attente des threads quitable ou non quitable. Avec tous ces points communs, on pourrait penser que Semaphore a t implmente partir de ReentrantLock ou que ReentrantLock la t partir dun Semaphore avec un seul permis. Ce serait tout fait possible et il sagit dun exercice classique pour prouver quun smaphore peut tre implment laide dun verrou (comme SemaphoreOnLock, dans le Listing 14.12) et quun verrou peut ltre partir dun smaphore.
Listing 14.12 : Semaphore implment partir de Lock.
// java.util.concurrent.Semaphore nest pas implmente de cette faon @ThreadSafe public class SemaphoreOnLock { private final Lock lock = new ReentrantLock(); // PRDICAT DE CONDITION : permitsAvailable (permits > 0)

1. ReentrantLock exige que le Lock soit dtenu lorsque lon appelle signal() ou signalAll(), mais les implmentations de Lock peuvent construire des Condition qui nont pas cette exigence.

316

Sujets avancs

Partie IV

Listing 14.12 : Semaphore implment partir de Lock. (suite)


private final Condition permitsAvailable = lock.newCondition(); @GuardedBy("lock") private int permits; SemaphoreOnLock(int initialPermits ) { lock.lock(); try { permits = initialPermits; } finally { lock.unlock(); } } // BLOQUE-JUSQU : permitsAvailable public void acquire() throws InterruptedException { lock.lock(); try { while (permits <= 0) permitsAvailable.await(); --permits; } finally { lock.unlock(); } } public void release() { lock.lock(); try { ++permits; permitsAvailable.signal(); } finally { lock.unlock(); } } }

En ralit, ces deux classes sont implmentes partir de la mme classe de base, AbstractQueuedSynchronizer (AQS) comme de nombreux autres synchronisateurs. AQS est un framework permettant de construire des verrous et un nombre impressionnant de synchronisateurs. ReentrantLock et Semaphore ne sont pas les seules classes construites partir dAQS : cest galement le cas de CountDownLatch, ReentrantReadWriteLock, SynchronousQueue1 et FutureTask. AQS gre la plupart des dtails dimplmentation dun synchronisateur, notamment le placement des threads en attente dans une le FIFO. Les diffrents synchronisateurs peuvent galement dnir des critres permettant de dterminer si un thread doit tre autoris passer ou forc dattendre. Lutilisation dAQS pour construire des synchronisateurs offre plusieurs avantages : non seulement on rduit leffort dimplmentation, mais on na pas payer le prix de plusieurs points de comptition ce qui serait le cas si lon construisait un synchronisateur partir dun autre. Dans SemaphoreOnLock, par exemple, lacquisition dun permis
1. Java 6 a remplac la classe SynchronousQueue qui reposait sur AQS par une version non bloquante (plus adaptable).

Chapitre 14

Construction de synchronisateurs personnaliss

317

comprend deux endroits potentiellement bloquants lorsque le verrou protge ltat du smaphore et lorsquil ny a plus de permis disponible. Les synchronisateurs construits avec AQS, en revanche, nont quun seul point de blocage potentiel, ce qui rduit le cot des changements de contexte et amliore le dbit. AQS a t conu pour tre adaptable et tous les synchronisateurs de java.util.concurrent qui reposent sur ce framework bncient de cette proprit.

14.5

AbstractQueuedSynchronizer

La plupart des dveloppeurs nutiliseront srement jamais AQS directement car lensemble standard des synchronisateurs disponibles couvre un assez grand nombre de situations. Cependant, savoir comment ces synchronisateurs sont implments permet de mieux comprendre leur fonctionnement. Les oprations de base dun synchronisateur reposant sur AQS sont des variantes de acquire() et release() : lacquisition est lopration qui dpend de ltat et peut donc se bloquer. Avec un verrou ou un smaphore, la signication de acquire() est vidente prendre le verrou ou un permis et lappelant peut devoir attendre que le synchronisateur soit dans un tat o cette opration peut russir. Avec CountDownLatch, acquire() signie "attend jusqu ce que le loquet ait atteint son tat nal" alors que, pour Future Task, elle signie "attend que la tche se soit termine". La mthode release() nest pas une opration bloquante ; elle permet aux threads bloqus par acquire() de poursuivre leur excution. Pour quune classe dpende de ltat, elle doit avoir un tat. AQS soccupe den grer une partie pour la classe synchronisateur : elle gre un unique entier faisant partie de ltat, qui peut tre manipul via les mthodes protges getState(), setState() et compareAndSetState(). Cet entier peut servir reprsenter un tat quelconque ; Reentrant Lock lutilise par exemple pour reprsenter le nombre de fois o le thread propritaire a pris le verrou, Semaphore sen sert pour reprsenter le nombre de permis restants et FutureTask, pour indiquer ltat de la tche (pas encore dmarre, en cours dexcution, termine, annule). Les synchronisateurs peuvent galement grer eux-mmes dautres variables dtat ; ReentrantLock mmorise par exemple le propritaire courant du verrou an de pouvoir faire la diffrence entre les requtes de verrou qui sont rentrantes et celles qui sont en comptition. Avec AQS, lacquisition et la libration ont la forme des mthodes prsentes dans le Listing 14.13. En fonction du synchronisateur, lacquisition peut tre exclusive, comme dans ReentrantLock, ou non exclusive, comme dans Semaphore et CountDownLatch. Une opration acquire() est forme de deux parties : le synchronisateur dcide dabord si ltat courant permet lacquisition, auquel cas le thread est autoris poursuivre ; sinon lopration bloque ou choue. Cette dcision est dtermine par la smantique du

318

Sujets avancs

Partie IV

synchronisateur : lacquisition dun verrou, par exemple, peut russir sil nest pas dj dtenu et lacquisition dun loquet peut russir sil est dans son tat nal.
Listing 14.13 : Formes canoniques de lacquisition et de la libration avec AQS.
boolean acquire() throws InterruptedException { while (ltat nautorise pas lacquisition) { if (blocage de lacquisition demand) { mettre le thread courant dans la file sil ny est pas dj bloquer le thread courant } else return chec } mettre ventuellement jour ltat de la synchronisation sortir le thread de la file sil tait dans la file return succs } void release() { mettre jour ltat de la synchronisation if (le nouvel tat peut permettre lacquisition un thread bloqu) dbloquer un ou plusieurs threads de la file }

La seconde partie implique de modier ventuellement ltat du synchronisateur ; un thread prenant le synchronisateur peut inuer sur le fait que dautres threads puissent le prendre. Acqurir un verrou, par exemple, fait passer ltat du verrou de "libre" "dtenu" et prendre un permis dun Semaphore rduit le nombre de permis restants. En revanche, lacquisition dun loquet par un thread naffecte pas le fait que dautres threads puissent le prendre : la prise dun loquet ne modie donc pas son tat. Un synchronisateur qui autorise une acquisition exclusive devrait implmenter les mthodes protges tryAcquire(), tryRelease() et isHeldExclusively() ; ceux qui reconnaissent lacquisition partage devraient implmenter tryAcquireShared() et tryReleaseShared(). Les mthodes acquire(), acquireShared(), release() et release Shared() dAQS appellent les formes try de ces mthodes dans la sous-classe synchronisateur an de dterminer si lopration peut avoir lieu. Cette sous-classe peut utiliser getState(), setState() et compareAndSetState() pour examiner et modier ltat en fonction de la smantique de ses mthodes acquire() et release() et se sert du code de retour pour informer la classe de base de la russite ou non de la tentative dacqurir ou de librer le synchronisateur. Si tryAcquireShared() renvoie une valeur ngative, par exemple, cela signie que lacquisition a chou ; une valeur nulle indique que le synchronisateur a t pris de faon exclusive et une valeur positive, quil a t pris de faon non exclusive. Les mthodes tryRelease() et tryReleaseShared() devraient renvoyer true si la libration a pu dbloquer des threads attendant de prendre le synchronisateur.

Chapitre 14

Construction de synchronisateurs personnaliss

319

Pour simplier limplmentation de verrous supportant les les dattente de condition (comme ReentrantLock), AQS fournit galement toute la mcanique permettant de construire des variables conditions associes des synchronisateurs. 14.5.1 Un loquet simple La classe OneShotLatch du Listing 14.14 est un loquet binaire implment laide dAQS. Il dispose de deux mthodes publiques, await() et signal(), qui correspondent respectivement lacquisition et la libration. Initialement, le loquet est ferm ; tout thread appelant await() se bloque jusqu ce que le loquet souvre. Lorsque le loquet est ouvert par un appel signal(), les threads en attente sont librs et ceux qui arrivent ensuite sont autoriss passer.
Listing 14.14 : Loquet binaire utilisant AbstractQueuedSynchronizer.
@ThreadSafe public class OneShotLatch { private final Sync sync = new Sync(); public void signal() { sync.releaseShared(0); } public void await() throws InterruptedException { sync.acquireSharedInterruptibly (0); } private class Sync extends AbstractQueuedSynchronizer { protected int tryAcquireShared(int ignored) { // Russit si le loquet est ouvert (state == 1), sinon choue return (getState() == 1)? 1: -1; } protected boolean tryReleaseShared(int ignored) { setState(1); // Le loquet est maintenant ouvert return true; // Les autres threads peuvent maintenant passer } } }

Dans OneShotLatch, ltat de lAQS contient ltat du loquet ferm (zro) ou ouvert (un). La mthode await() appelle acquireSharedInterruptibly() dAQS, qui, son tour, consulte la mthode tryAcquireShared() de OneShotLatch. Limplmentation de tryAcquireShared() doit renvoyer une valeur indiquant si lacquisition peut ou non avoir lieu. Si le loquet a dj t ouvert, tryAcquireShared() renvoie une valeur indiquant le succs de lopration, ce qui autorise le thread passer ; sinon elle renvoie une valeur signalant que la tentative dacquisition a chou. Dans ce dernier cas, acquireShared Interruptibly() place le thread dans la le des threads en attente. De mme, signal() appelle releaseShared(), qui provoque la consultation de tryReleaseShared(). Limplmentation de tryReleaseShared() xe inconditionnellement ltat du loquet "ouvert" et indique (via sa valeur de retour) que le synchronisateur est dans un tat totalement libre. Ceci force AQS laisser tous les threads en attente tenter de reprendre

320

Sujets avancs

Partie IV

le synchronisateur, et lacquisition russira alors car tryAcquireShared() renvoie une valeur indiquant le succs de lopration.
OneShotLatch est un synchronisateur totalement fonctionnel et performant, implment en une vingtaine de lignes de code seulement. Il manque bien sr quelques fonctionnalits utiles, comme une acquisition avec dlai dexpiration ou la possibilit dinspecter ltat du loquet, mais elles sont relativement faciles implmenter car AQS fournit des versions avec dlais des mthodes dacquisition, ainsi que des mthodes utilitaires pour les oprations dinspection classiques. OneShotLatch aurait pu tre ralise en tendant AQS plutt quen lui dlguant des oprations, mais ce nest pas souhaitable pour plusieurs raisons [EJ Item 14]. Notamment, cette pratique dtriorerait linterface simple (deux mthodes seulement) de OneShotLatch et, bien que les mthodes publiques dAQS ne permettent pas aux appelants de perturber ltat du loquet, ceux-ci pourraient aisment les utiliser indirectement. Aucun des synchronisateurs de java.util.concurrent ne drive directement dAQS ils dlguent tous leurs oprations des sous-classes internes prives dAQS.

14.6

AQS dans les classes de java.util.concurrent

De nombreuses classes bloquantes de java.util.concurrent, comme ReentrantLock, Semaphore, ReentrantReadWriteLock, CountDownLatch, SynchronousQueue et Future Task, sont construites partir dAQS. Sans aller trop loin dans les dtails (le code source est inclus dans le tlchargement1 du JDK), tudions rapidement comment chacune de ces classes tire parti dAQS. 14.6.1 ReentrantLock
ReentrantLock ne supporte que lacquisition exclusive et implmente donc tryAcquire(), tryRelease() et isHeldExclusively() ; le Listing 14.15 montre la version non quitable de tryAcquire(). ReentrantLock utilise ltat de la synchronisation pour mmoriser le nombre dacquisitions de verrous et gre une variable owner contenant lidentit du thread propritaire, qui nest modie que lorsque le thread courant vient de prendre le verrou ou quil va le librer2. Dans tryRelease(), on vrie le champ owner pour sassurer que le thread courant possde le verrou avant dautoriser une libration ; dans
1. Ou, avec moins de contraintes de licence, partir de http://gee.cs.oswego.edu/dl/concurrencyinterest. 2. Les mthodes protges de manipulation de ltat ont la smantique mmoire dune lecture ou dune criture de volatile et ReentrantLock prend soin de ne lire le champ owner quaprs avoir appel getState() et de lcrire avant lappel de setState() ; ReentrantLock peut englober la smantique mmoire de ltat de synchronisation et ainsi viter une synchronisation supplmentaire voir la section 16.1.4.

Chapitre 14

Construction de synchronisateurs personnaliss

321

tryAcquire(), ce champ sert diffrencier une acquisition rentrante dune tentative de prise de verrou avec comptition.
Listing 14.15 : Implmentation de tryAcquire() pour un ReentrantLock non quitable.
protected boolean tryAcquire(int ignored) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, 1)) { owner = current; return true; } } else if (current == owner) { setState(c+1); return true; } return false; }

Lorsquun thread tente de prendre un verrou, tryAcquire() consulte dabord ltat de ce verrou. Sil est disponible, la mthode tente de mettre jour cet tat pour indiquer quil est pris. Comme il pourrait avoir t modi depuis sa consultation, tryAcquire() utilise compareAndSetState() pour essayer deffectuer de faon atomique la modication signalant que le verrou est dsormais dtenu et pour conrmer que ltat na pas t modi depuis sa dernire consultation (voir la description de compareAndSet() dans la section 15.3). Si ltat du verrou indique quil est dj pris et quil appartient au thread courant, le compteur dacquisition est incrment ; si le thread courant nest pas son propritaire, la tentative choue.
ReentrantLock tire galement parti du fait quAQS sait grer les variables conditions et les ensembles en attente. Lock.newCondition() renvoie une nouvelle instance de ConditionObject, une classe interne dAQS.

14.6.2 Semaphore et CountDownLatch


Semaphore utilise ltat de synchronisation dAQS pour stocker le nombre de permis disponibles. La mthode tryAcquireShared() (voir Listing 14.16) commence par calculer

le nombre de permis restants ; sil ny en a plus assez, elle renvoie une valeur indiquant que lacquisition a chou. Sil reste assez de permis, elle tente de diminuer de faon atomique leur nombre laide de compareAndSetState(). Si lopration russit (ce qui signie que le nombre de permis na pas t modi depuis sa dernire consultation), elle renvoie une valeur indiquant que lacquisition a russi. Cette valeur de retour prcise galement si une autre tentative dacquisition partage pourrait russir, auquel cas les autres threads en attente seront galement dbloqus.

322

Sujets avancs

Partie IV

Listing 14.16 : Les mthodes tryAcquireShared() et tryReleaseShared() de Semaphore.


protected int tryAcquireShared (int acquires) { while (true) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState (available, remaining)) return remaining; } } protected boolean tryReleaseShared (int releases) { while (true) { int p = getState(); if (compareAndSetState (p, p + releases)) return true; } }

La boucle while se termine lorsquil ny a plus assez de permis ou lorsque tryAcquire Shared() peut modier de faon atomique le nombre de permis pour reter lacquisition. Bien que tout appel compareAndSetState() puisse chouer cause dune comptition avec un autre thread (voir la section 15.3), ce qui provoquera un ressai, lun de ces deux critres de sortie deviendra vrai aprs un nombre raisonnable de tentatives. De mme, tryReleaseShared() augmente le nombre de permis ce qui dbloquera ventuellement des threads en attente en ressayant jusqu ce que cette mise jour russisse. La valeur renvoye par tryReleaseShared() indique si dautres threads pourraient avoir t dbloqus par cette libration.
CountDownLatch utilise AQS un peu comme Semaphore : ltat de synchronisation contient le compte courant. La mthode countDown() appelle release(), qui dcrmente ce compteur et dbloque les threads en attente sil a atteint zro ; await() appelle acquire(), qui se termine immdiatement si le compteur vaut zro ou se bloque dans le cas contraire.

14.6.3 FutureTask Au premier abord, FutureTask ne ressemble pas un synchronisateur. Cependant, la smantique de Future.get() ressemble fortement celle dun loquet si un vnement (la terminaison ou lannulation de la tche reprsente par la FutureTask) survient, les threads peuvent poursuivre leur excution ; sinon ils sont mis en attente de cet vnement.
FutureTask utilise ltat de synchronisation dAQS pour stocker ltat de la tche en cours dexcution, termine ou annule. Elle gre galement des variables dtat supplmentaires pour mmoriser le rsultat du calcul ou lexception quelle a lance. Elle utilise en outre une rfrence au thread qui excute le calcul (sil est dans ltat "en cours dexcution") an de pouvoir linterrompre si la tche est annule.

14.6.4 ReentrantReadWriteLock Linterface de ReadWriteLock suggre quil y a deux verrous un verrou de lecture et un dcriture mais, dans son implmentation partir dAQS, une seule sous-classe

Chapitre 14

Construction de synchronisateurs personnaliss

323

dAQS gre ces deux verrouillages. Pour ce faire, ReentrantReadWriteLock utilise 16 bits de ltat pour stocker le compteur du verrou dcriture et les 16 autres pour le verrou de lecture. Les oprations sur le verrou de lecture utilisent les mthodes dacquisition et de libration partages ; celles sur le verrou dcriture, les versions exclusives de ces mthodes. En interne, AQS gre une le des threads en attente et mmorise si un thread a demand un accs exclusif ou partag. Avec ReentrantReadWriteLock, lorsque le verrou devient disponible, si le verrou en tte de la le veut un accs en criture il lobtient ; sil veut un accs en lecture tous les threads de la le jusquau premier thread crivain lobtiendront1.

Rsum
Si vous devez implmenter une classe dpendante de ltat une classe dont les mthodes doivent se bloquer si une prcondition reposant sur ltat nest pas vrie , la meilleure stratgie consiste gnralement partir dune classe existante de la bibliothque, comme Semaphore, BlockingQueue ou CountDownLatch, comme on le fait pour ValueLatch au Listing 8.17. Cependant, ces classes ne fournissent pas toujours un point de dpart sufsant ; dans ce cas, vous pouvez construire vos propres synchronisateurs en utilisant les les dattente de condition internes, des objets Condition explicites ou Abstract QueuedSynchronizer. Les les dattente de condition internes sont intimement lies au verrouillage interne puisque le mcanisme de gestion des dpendances vis--vis de ltat est ncessairement li celui qui garantit la cohrence de cet tat. De mme, les Condition explicites sont troitement lies aux Lock explicites et offrent un ensemble de fonctionnalits plus tendu que celui de leurs homologues internes, notamment des ensembles dattentes par verrou, des attentes de condition interruptibles ou non interruptibles, des attentes quitables ou non quitables et des attentes avec dlai dexpiration.

1. Contrairement certaines implmentations des verrous de lecture et dcriture, ce mcanisme ne permet pas de choisir entre une politique donnant la prfrence aux lecteurs et une autre donnant la prfrence aux crivains. Pour cela, la le dattente dAQS ne devrait pas tre une le FIFO ou il faudrait deux les. Cependant, une politique dordonnancement aussi stricte est rarement ncessaire en pratique ; si la vivacit de la version non quitable de ReentrantReadWriteLock ne suft pas, la version quitable fournit gnralement un ordre satisfaisant et garantit que les lecteurs et les crivains ne souffriront pas de famine.

15
Variables atomiques et synchronisation non bloquante
De nombreuses classes de java.util.concurrent, comme Semaphore et Concurrent LinkedQueue, ont de meilleures performances et une plus grande adaptabilit que les alternatives qui utilisent synchronized. Dans ce chapitre, nous tudierons la source principale de ce gain de performances : les variables atomiques et la synchronisation non bloquante. Lessentiel des recherches rcentes sur les algorithmes concurrents est consacr aux algorithmes non bloquants, qui assurent lintgrit des donnes lors daccs concurrents laide dinstructions machines atomiques de bas niveau, comme comparer-et-changer, au lieu de faire appel des verrous. Ces algorithmes sont trs frquemment utiliss dans les systmes dexploitation et les JVM pour planier lexcution des threads et des processus, pour le ramasse-miettes et pour implmenter les verrous et autres structures de donnes concurrentes. Ces algorithmes sont beaucoup plus compliqus concevoir et implmenter que ceux qui utilisent des verrous, mais ils offrent une adaptabilit et des avantages en termes de vivacit non ngligeables. Leur synchronisation a une granularit plus ne et permet de rduire normment le cot de la planication car ils ne se bloquent pas lorsque plusieurs threads concourent pour les mmes donnes. En outre, ils sont immuniss contre les interblocages et autres problmes de vivacit. Avec les algorithmes qui reposent sur les verrous, les autres threads ne peuvent pas progresser si un thread se met en sommeil ou en boucle pendant quil dtient le verrou, alors que les algorithmes non bloquants sont impermables aux checs des diffrents threads. partir de Java 5.0, il est possible de construire ces algorithmes efcacement en Java laide des classes de variables atomiques, comme AtomicInteger et AtomicReference.

326

Sujets avancs

Partie IV

Les variables atomiques peuvent galement servir de "meilleures variables volatiles", mme si vous ncrivez pas dalgorithme non bloquant. Elles offrent en effet la mme smantique mmoire que les variables volatiles mais lui ajoutent les modications atomiques, ce qui en fait les solutions idales pour les compteurs, les gnrateurs de squence et les collectes de statistiques tout en offrant une meilleure adaptabilit que leurs alternatives avec verrou.

15.1

Inconvnients du verrouillage

Une synchronisation de laccs ltat partag laide dun protocole de verrouillage cohrent garantit que le thread qui dtient le verrou qui protge un ensemble de variables dispose dun accs exclusif celles-ci et que toutes les modications qui leur sont apportes seront visibles aux threads qui prendront ensuite le verrou. Les JVM actuelles peuvent optimiser assez efcacement les acquisitions de verrou qui ne donnent pas lieu comptition, ainsi que leur libration, mais, si plusieurs threads demandent le verrou en mme temps, la JVM a besoin de laide du systme dexploitation. En ce cas, le thread qui a perdu la bataille sera suspendu et devra reprendre plus tard1. Lorsque ce thread reprend son excution, il peut devoir attendre que les autres threads terminent leurs quanta de temps avant de pouvoir lui-mme tre plani, or la suspension et la reprise dun thread cote cher et implique gnralement une longue interruption. Pour les classes qui utilisent des verrous avec des oprations courtes (comme les classes collections synchronises, dont la plupart des mthodes ne contiennent que quelques oprations), le rapport entre le cot de la planication et le travail utile peut tre assez lev si le verrou est souvent disput. Les variables volatiles constituent un mcanisme de synchronisation plus lger que le verrouillage car elles nimpliquent pas de changement de contexte ni de planication des threads. Cependant, elles prsentent quelques inconvnients par rapport aux verrous : bien quelles fournissent les mmes garanties de visibilit, elles ne peuvent pas servir construire des actions composes atomiques. Ceci signie que lon ne peut pas utiliser les variables volatiles lorsquune variable dpend dune autre ou que sa nouvelle valeur dpend de lancienne. Cet inconvnient limite lutilisation des variables volatiles puisquelles ne peuvent pas servir implmenter correctement des outils classiques comme les compteurs ou les mutex2. Bien que lopration dincrmentation ( ++i) puisse, par exemple, ressembler une opration atomique, il sagit en fait de trois oprations distinctes rcuprer la valeur
1. Une JVM intelligente na pas ncessairement besoin de suspendre un thread qui lutte pour obtenir un verrou : elle pourrait utiliser des donnes de prolage pour choisir entre une suspension et une boucle en fonction du temps pendant lequel le verrou a t dtenu au cours des acquisitions prcdentes. 2. Il est thoriquement possible, bien que totalement impraticable, dutiliser la smantique de volatile pour construire des mutex ou dautres synchronisateurs ; voir (Raynal, 1986).

Chapitre 15

Variables atomiques et synchronisation non bloquante

327

courante de la variable, lui ajouter un, puis crire le rsultat dans la variable. Pour ne pas perdre une mise jour, toute lopration lecture-modication-criture doit tre atomique. Pour linstant, le seul moyen dy parvenir que nous avons tudi consiste utiliser un verrou, comme dans la classe Counter au Listing 4.1.
Counter est thread-safe et fonctionne bien en cas de comptition faible ou inexistante. Dans le cas contraire, cependant, les performances souffrent du cot des changements de contexte et des dlais induits par la planication. Lorsque les verrous sont utiliss aussi brivement, tre mis en sommeil est une punition svre pour avoir demand le verrou au mauvais moment.

Le verrouillage a dautres inconvnients. Notamment, un thread qui attend un verrou ne peut rien faire dautre. Si un thread qui dtient un verrou est retard ( cause dune faute de page, dun dlai de planication, etc.), aucun autre thread ayant besoin du verrou ne peut progresser. Ce problme peut tre srieux si le thread bloqu a une priorit forte alors que celui qui dtient le verrou a une priorit faible cette situation est appele inversion des priorits. Mme si le thread de forte priorit devrait tre prioritaire, il doit attendre que le verrou soit relch, ce qui revient ramener sa priorit celle du thread de priorit plus basse. Si un thread qui dtient un verrou est dnitivement bloqu ( cause dune boucle sans n, dun interblocage, dun livelock ou de tout autre chec de vivacit), tous les threads qui attendent le verrou ne pourront plus jamais progresser. Mme en ignorant ces problmes, le verrouillage est tout simplement un mcanisme lourd pour des oprations courtes comme lincrmentation dun compteur. Il serait plus pratique de disposer dune technique plus lgre pour grer la comptition entre les threads quelque chose comme les variables volatiles, mais avec la possibilit de modication atomique en plus. Heureusement, les processeurs modernes nous offrent prcisment ce mcanisme.

15.2

Support matriel de la concurrence

Le verrouillage exclusif est une technique pessimiste elle suppose le pire (si vous ne fermez pas votre porte, les gremlins viendront et mettront tout sens dessus dessous) et ne progresse pas tant que vous ne pouvez pas lui garantir (en prenant les verrous adquats) que dautres threads ninterfreront pas. Pour les oprations courtes, il existe une autre approche qui est souvent plus efcace lapproche optimiste, dans laquelle on effectue une mise jour en esprant quelle pourra se terminer sans interfrence. Cette approche repose sur la dtection des collisions pour dterminer sil y a eu interfrence avec dautres parties au cours de la mise jour, auquel cas lopration choue et peut (ou non) tre tente nouveau. Lapproche optimiste ressemble donc au vieux dicton "il est plus facile de se faire pardonner que de demander la permission", o "plus facile" signie ici "plus efcace".

328

Sujets avancs

Partie IV

Les processeurs conus pour les oprations en parallle disposent dinstructions spciales pour grer les accs concurrents aux variables partages. Les anciens processeurs disposaient des instructions test-and-set, fetch-and-increment ou swap, qui sufsaient implmenter des mutex pouvant, leur tour, servir crer des objets concurrents plus sophistiqus. Aujourdhui, quasiment tous les processeurs modernes ont une instruction de lecture-modication-criture atomique, comme compare-and-swap ou load-linked/ store-conditional. Les systmes dexploitation et les JVM les utilisent pour implmenter les verrous et les structures de donnes concurrentes mais, jusqu Java 5.0, elles ntaient pas directement accessibles aux classes Java. 15.2.1 Linstruction Compare-and-swap Lapproche choisie par la plupart des architectures de processeurs, notamment IA32 et Sparc, consiste implmenter linstruction compare-and-swap (CAS) ; les autres, comme le PowerPC, obtiennent la mme fonctionnalit avec une paire dinstructions, load-linked et store-conditional. Linstruction CAS porte sur trois oprandes un emplacement mmoire V, lancienne valeur A et la nouvelle valeur B. CAS modie de faon atomique V pour quelle reoive la valeur B uniquement si la valeur contenue dans V correspond lancienne valeur A ; sinon elle ne fait rien. Dans les deux cas, elle renvoie la valeur courante de V (la variante compare-and-set renvoie une valeur indiquant si lopration a russi). CAS signie donc "je pense que V vaut A ; si cest le cas, jy place B, sinon je ne modie rien car javais tort". CAS est donc une technique optimiste elle effectue la mise jour en pensant russir et sait dtecter lchec si un autre thread a modi la variable depuis son dernier examen. La classe SimulatedCAS du Listing 15.1 illustre la smantique (mais ni limplmentation ni les performances) de CAS.
Listing 15.1 : Simulation de lopration CAS.
@ThreadSafe public class SimulatedCAS { @GuardedBy("this") private int value; public synchronized int get() { return value; } public synchronized int compareAndSwap(int expectedValue , int newValue) { int oldValue = value; if (oldValue == expectedValue ) value = newValue; return oldValue; } public synchronized boolean compareAndSet(int expectedValue , int newValue) { return (expectedValue == compareAndSwap(expectedValue, newValue)); } }

Lorsque plusieurs threads tentent de mettre jour simultanment la mme variable avec CAS, lun gagne et modie la valeur de la variable, les autres perdent. Cependant, les

Chapitre 15

Variables atomiques et synchronisation non bloquante

329

perdants ne sont pas punis par une suspension, comme cela aurait t le cas sils avaient chou dans la prise dun verrou : on leur indique simplement quils ont perdu la course cette fois-ci mais quils peuvent ressayer. Un thread ayant chou avec CAS ntant pas bloqu, il peut dcider quand recommencer sa tentative, effectuer une action de repli ou ne rien faire1. Cette souplesse limine la plupart des checs de vivacit lis au verrouillage (bien que, dans certains cas rares, elle puisse introduire le risque de livelocks voir la section 10.3.3). Le patron dutilisation typique de CAS consiste dabord lire la valeur A qui est dans V, construire la nouvelle valeur B partir de A puis utiliser CAS pour changer de faon atomique la valeur de A par B dans V si aucun autre thread na chang la valeur de V entre-temps. Linstruction CAS gre le problme de limplmentation de la squence lecture-modication-criture atomique sans verrou car elle peut dtecter les interfrences provenant des autres threads. 15.2.2 Compteur non bloquant La classe CasCounter du Listing 15.2 implmente un compteur thread-safe qui utilise CAS. Lopration dincrmentation respecte la forme canonique rcuprer lancienne valeur, la transformer en nouvelle valeur (en lui ajoutant un) et utiliser CAS pour mettre la nouvelle valeur. Si CAS choue, lopration est immdiatement retente. Les tentatives rptition sont une stratgie raisonnable bien que, en cas de comptition trs forte, il puisse tre prfrable dattendre pour viter un livelock.
Listing 15.2 : Compteur non bloquant utilisant linstruction CAS.
@ThreadSafe public class CasCounter { private SimulatedCAS value; public int getValue() { return value.get(); } public int increment() { int v; do { v = value.get(); } while (v != value.compareAndSwap (v, v + 1)); return v + 1; } }

1. Ne rien faire est une rponse tout fait sense lchec de CAS ; dans certains algorithmes non bloquants, comme celui de la le chane de la section 15.4.2, lchec de CAS signie que quelquun dautre a dj fait le travail que lon comptait faire.

330

Sujets avancs

Partie IV

CasCounter ne se bloque pas, bien quil puisse devoir essayer plusieurs 1 fois si dautres threads modient le compteur au mme moment (en pratique, si vous avez simplement besoin dun compteur ou dun gnrateur de squence, il suft dutiliser AtomicInteger ou AtomicLong, qui fournissent lincrmentation automatique et dautres mthodes arithmtiques).

Au premier abord, le compteur qui utilise CAS semble tre moins performant que le compteur utilisant le verrouillage : il y a plus doprations, son ux de contrle est plus complexe et dpend dune opration CAS apparemment complique. Cependant, en ralit, les compteurs qui utilisent CAS ont des performances bien suprieures ds lapparition dune comptition, mme faible et souvent meilleures lorsquil ny en a pas. Le chemin le plus rapide pour une acquisition dun verrou non disput ncessite gnralement au moins une opration CAS, plus dautres oprations de maintenance lies au verrou : il faut donc faire plus de travail pour les compteurs base de verrou dans le meilleur des cas que dans le cas normal pour les compteurs utilisant CAS. Lopration CAS russissant la plupart du temps (avec une comptition moyenne), le processeur prdira correctement le branchement implicite dans la boucle while, ce qui minimisera le cot de la logique de contrle plus complexe. Bien que la syntaxe du langage pour le verrouillage soit compacte, ce nest pas le cas du travail effectu par la JVM et le systme dexploitation pour grer les verrous. Le verrouillage implique en effet lexcution dun code relativement complexe dans la JVM et peut galement impliquer un verrouillage, une suspension de thread et des changes de contextes au niveau du systme dexploitation. Dans le meilleur des cas, le verrouillage exige au moins une instruction CAS : si les verrous cachent CAS, ils nconomisent donc pas son cot. Lexcution de CAS partir dun programme, en revanche, nimplique aucun code supplmentaire dans la JVM, aucun autre appel systme ni aucune activit de planication. Ce qui ressemble un code plus long au niveau de lapplication est donc en ralit un code beaucoup plus court si lon prend en compte les activits de la JVM et du SE. Linconvnient principal de CAS est quelle force lappelant grer la comptition (en ressayant, en revenant en arrire ou en abandonnant) alors que le verrouillage la traite automatiquement en bloquant jusqu ce que le verrou devienne disponible 2. Les performances de CAS varient en fonction des processeurs. Sur un systme monoprocesseur, une instruction CAS sexcute gnralement en quelques cycles dhorloge puisquil ny a pas besoin de synchronisation entre les processeurs. Actuellement, le cot dune instruction CAS non dispute sur un systme multiprocesseur prend de 10 150 cycles ; les performances de CAS voluent rapidement et dpendent non seulement
1. Thoriquement, il pourrait devoir ressayer un nombre quelconque de fois si les autres threads continuent de gagner la course de CAS ; en pratique, ce type de famine arrive rarement. 2. En fait, le plus gros inconvnient de CAS est la difcult construire correctement les algorithmes qui lutilisent.

Chapitre 15

Variables atomiques et synchronisation non bloquante

331

des architectures, mais galement des versions du mme processeur. Ces performances continueront srement de samliorer au cours des prochaines annes. Une bonne estimation est que le cot dune acquisition "rapide" dun verrou non disput et de sa libration est environ le double de celui de CAS sur la plupart des processeurs. 15.2.3 Support de CAS dans la JVM Comment Java fait-il pour convaincre le processeur dexcuter une instruction CAS pour lui ? Avant Java 5.0, il ny avait aucun moyen de le faire, part crire du code natif. Avec Java 5.0, un accs de bas niveau a t ajout an de pouvoir lancer des oprations CAS sur les int, les long, les rfrences dobjet, que la JVM compile an dobtenir le plus defcacit possible avec le matriel sous-jacent. Sur les plates-formes qui disposent de CAS, ces instructions sont traduites dans les instructions machine appropries ; dans le pire des cas, la JVM utilisera un verrou avec attente active si une instruction de type CAS nest pas disponible. Ce support de bas niveau est utilis par les classes de variables atomiques (AtomicXxx dans java.util.concurrent.atomic) an de fournir une opration CAS efcace sur les types numriques et rfrence ; ces classes sont utilises directement ou indirectement pour implmenter la plupart des classes de java.util.concurrent.

15.3

Classes de variables atomiques

Les variables atomiques sont des mcanismes plus ns et plus lgers que les verrous et sont essentiels pour implmenter du code concurrent trs performant sur les systmes multiprocesseurs. Les variables atomiques limitent la porte de la comptition une unique variable ; la granularit est donc la plus ne quil est possible dobtenir (en supposant que votre algorithme puisse tre implment en utilisant une granularit aussi ne). Le chemin rapide (sans comptition) pour modier une variable atomique nest pas plus lent que le chemin rapide pour acqurir un verrou, et il est mme gnralement plus rapide ; le chemin lent est toujours plus rapide que le chemin lent des verrous car il nimplique pas de suspendre et de replanier les threads. Avec des algorithmes utilisant les variables atomiques la place des verrous, les threads peuvent plus facilement sexcuter sans dlai et revenir plus vite dans la course sils ont t pris dans une comptition. Les classes de variables atomiques sont une gnralisation des variables volatiles pour quelles disposent doprations atomiques conditionnelles de type lecture-modicationcriture. AtomicInteger reprsente une valeur de type int et fournit les mthodes get() et set(), qui ont les mmes smantiques que les lectures et les critures dune variable int volatile. Elle fournit galement une mthode compareAndSet() atomique (qui, si elle russit, a les effets mmoire dune lecture et dune criture dans une variable volatile) et les mthodes utilitaires add(), increment() et decrement(), toutes atomiques. Cette classe ressemble donc un peu une classe Counter tendue mais offre une bien meilleure

332

Sujets avancs

Partie IV

adaptabilit en cas de comptition car elle peut exploiter directement les fonctionnalits quutilise le matriel sous-jacent en cas de concurrence. Il existe douze classes de variables atomiques divises en quatre groupes : les scalaires, les modicateurs de champs, les tableaux et les variables composes. Les variables atomiques les plus frquemment utilises appartiennent la catgorie des scalaires : AtomicInteger, AtomicLong, AtomicBoolean et AtomicReference. Toutes supportent CAS ; les versions Integer et Long disposent galement des oprations arithmtiques (pour simuler des variables atomiques pour les autres types primitifs, vous pouvez transtyper les valeurs short et byte en int ou inversement et utiliser floatToIntBits() ou doubleToLongBits() pour les nombres virgule ottante). Les classes de tableaux atomiques (disponibles en versions Integer, Long et Reference) reprsentent des tableaux dont les lments peuvent tre modis de faon atomique. Elles fournissent la smantique des accs volatiles aux lments du tableau, ce qui nest pas le cas des tableaux ordinaires un tableau volatile na une smantique volatile que pour sa rfrence, pas pour ses lments (les autres types de variables atomiques seront prsents dans les sections 15.4.3 et 15.4.4). Bien que les classes atomiques scalaires hritent de Number, elles nhritent pas des classes enveloppes des types primitifs comme Integer ou Long. En fait, elles ne le peuvent pas : ces classes enveloppes ne sont pas modiables alors que les classes de variables atomiques le sont. En outre, ces dernires ne rednissent pas les mthodes hashCode() et equals() ; chaque instance est distincte. Comme avec la plupart des objets modiables, il nest donc pas conseill dutiliser une variable atomique comme cl dune collection de type hachage. 15.3.1 Variables atomiques comme "volatiles amliores" Dans la section 3.4.2, nous avons utilis une rfrence volatile vers un objet non modiable pour mettre jour plusieurs variables dtat de faon atomique. Cet exemple utilisait une opration compose de type tester-puis-agir mais, dans ce cas particulier, la situation de comptition ne posait pas de problme puisque nous ne nous soucions pas de perdre une modication de temps en temps. La plupart du temps, cependant, une telle opration poserait problme car elle pourrait compromettre lintgrit des donnes. La classe NumberRange du Listing 4.10, par exemple, ne pourrait pas tre implmente de faon thread-safe avec une rfrence volatile vers un objet non modiable pour les limites infrieure et suprieure, ni avec des entiers atomiques pour stocker ces limites. Un invariant contraignant les deux nombres qui ne peuvent pas tre modis simultanment sans violer cet invariant, les squences tester-puis-agir dune classe dintervalle de nombres utilisant des rfrences volatiles ou plusieurs entiers atomiques ne seront pas thread-safe. Nous pouvons combiner la technique utilise pour OneValueCache et les rfrences atomiques pour mettre n la situation de comptition en modiant de faon atomique la rfrence vers un objet non modiable contenant les limites infrieure et suprieure.

Chapitre 15

Variables atomiques et synchronisation non bloquante

333

La classe CasNumberRange du Listing 15.3 utilise une AtomicReference vers un objet IntPair contenant ltat ; grce compareAndSet(), elle peut mettre jour la limite infrieure ou suprieure sans risquer la situation de comptition de NumberRange.
Listing 15.3 : Prservation des invariants multivariables avec CAS.
public class CasNumberRange { @Immutable private static class IntPair { final int lower; // Invariant : lower <= upper final int upper; ... } private final AtomicReference <IntPair> values = new AtomicReference <IntPair>(new IntPair(0, 0)); public int getLower() { return values.get().lower; } public int getUpper() { return values.get().upper; } public void setLower(int i) { while (true) { IntPair oldv = values.get(); if (i > oldv.upper) throw new IllegalArgumentException("Cant set lower to " + i + " > upper"); IntPair newv = new IntPair(i, oldv.upper); if (values.compareAndSet(oldv, newv)) return; } } // idem pour setUpper() }

15.3.2 Comparaison des performances des verrous et des variables atomiques Pour dmontrer les diffrences dadaptabilit entre les verrous et les variables atomiques, nous avons construit un programme de test comparant plusieurs implmentations dun gnrateur de nombres pseudo-alatoires (GNPA). Dans un GNPA, le nombre "alatoire" suivant tant une fonction dterministe du nombre prcdent, un GNPA doit mmoriser ce nombre prcdent comme faisant partie de son tat. Les Listings 15.4 et 15.5 montrent deux implmentations dun GNPA thread-safe, lune utilisant ReentrantLock, lautre, AtomicInteger. Le pilote de test appelle chacune delles de faon rpte ; chaque itration gnre un nombre alatoire (qui lit et modie ltat partag seed) et effectue un certain nombre ditrations "de travail" qui agissent uniquement sur des donnes locales au thread. Ce programme simule des oprations typiques qui incluent une partie consistant agir sur ltat partag et une autre qui opre sur ltat local au thread.

334

Sujets avancs

Partie IV

Listing 15.4 : Gnrateur de nombres pseudo-alatoires avec ReentrantLock.


@ThreadSafe public class ReentrantLockPseudoRandom extends PseudoRandom { private final Lock lock = new ReentrantLock(false); private int seed; ReentrantLockPseudoRandom(int seed) { this.seed = seed; } public int nextInt(int n) { lock.lock(); try { int s = seed; seed = calculateNext(s); int remainder = s % n; return remainder > 0 ? remainder : remainder + n; } finally { lock.unlock(); } } }

Listing 15.5 : Gnrateur de nombres pseudo-alatoires avec AtomicInteger.


@ThreadSafe public class AtomicPseudoRandom extends PseudoRandom { private AtomicInteger seed; AtomicPseudoRandom(int seed) { this.seed = new AtomicInteger(seed); } public int nextInt(int n) { while (true) { int s = seed.get(); int nextSeed = calculateNext(s); if (seed.compareAndSet(s, nextSeed)) { int remainder = s % n; return remainder > 0 ? remainder : remainder + n; } } } }

Les Figures 15.1 et 15.2 montrent le dbit avec des niveaux faible et moyen de travail chaque itration. Avec un peu de calcul local au thread, le verrou ou les variables atomiques affrontent une forte comptition ; avec plus de calcul local au thread, le verrou ou les variables atomiques rencontrent moins de comptition puisque chaque thread y accde moins souvent.

Chapitre 15

Variables atomiques et synchronisation non bloquante

335

Figure 15.1
Performances de Lock et AtomicInteger en cas de forte comptition.
Dbit (normalis) 5

3 ThreadLocal 2

1 ReentrantLock AtomicInteger 1 2 4 8 16 32 64

0 Nombre de threads

Figure 15.2
Performances de Lock et AtomicInteger en cas de comptition modre.
5

ThreadLocal

Dbit (normalis)

3 AtomicInteger 2

1 ReentrantLock 0 1 2 4 8 16 32 64 Nombre de threads

Comme le montrent ces graphiques, des niveaux de forte comptition le verrouillage a tendance tre plus performant que les variables atomiques mais, des niveaux de comptition plus ralistes, ce sont ces dernires qui prennent lavantage 1. En effet, un verrou ragit la comptition en suspendant les threads, ce qui rduit lutilisation processeur et le trac de synchronisation sur le bus de la mmoire partage (de la mme faon que le blocage des producteurs dans une architecture producteur-consommateur rduit la charge sur les consommateurs et leur permet ainsi de rcuprer). Avec les

1. Cest galement vrai dans dautres domaines : les feux de circulation ont un meilleur rendement en cas de fort trac mais ce sont les ronds-points qui lemportent en cas de trac faible ; le schma de comptition utilis par les rseaux Ethernet se comporte mieux en cas de faible trac alors que cest le schma du passage de jeton utilis par Token Ring qui est le plus efcace pour un trac rseau lev.

336

Sujets avancs

Partie IV

variables atomiques, en revanche, la gestion de la comptition est repousse dans la classe appelante. Comme la plupart des algorithmes qui utilisent CAS, AtomicPseudo Random ragit la comptition en ressayant immdiatement, ce qui est gnralement la bonne approche mais qui, dans un environnement trs disput, ne fait quaugmenter la comptition. Avant de considrer que AtomicPseudoRandom est mal crite ou que les variables atomiques sont un mauvais choix par rapport aux verrous, il faut se rendre compte que le niveau de comptition de la Figure 5.1 est exagrment haut : aucun programme ne se consacre exclusivement lutter pour la prise dun verrou ou pour laccs une variable atomique. En pratique, les variables atomiques sadaptent mieux que les verrous parce quelles grent plus efcacement les niveaux typiques de comptition. Le renversement de performances entre les verrous et les variables atomiques aux diffrents niveaux de comptition illustre les forces et les faiblesses de ces deux approches. Avec une comptition faible modre, les variables atomiques offrent une meilleure adaptabilit ; avec une forte comptition, les verrous offrent souvent un meilleur comportement (les algorithmes qui utilisent CAS sont galement plus performants que les verrous sur les systmes monoprocesseurs puisque CAS russit toujours, sauf dans le cas peu probable o un thread est prempt au milieu dune opration de lecture-modicationcriture). Les Figures 15.1 et 15.2 contiennent une troisime courbe qui reprsente le dbit dune implmentation de PseudoRandom utilisant un objet ThreadLocal pour ltat du GNPA. Cette approche modie le comportement de la classe au lieu que tous les threads partagent la mme squence, chaque thread dispose de sa propre squence prive de nombres pseudo-alatoires mais illustre le fait quil est souvent plus conomique de ne pas partager du tout ltat si on peut lviter. Nous pouvons amliorer ladaptabilit en grant plus efcacement la comptition, mais une vritable adaptabilit ne peut sobtenir quen liminant totalement cette comptition.

15.4

Algorithmes non bloquants

Les algorithmes qui utilisent des verrous peuvent souffrir dun certain nombre de problmes de vivacit. Si un thread qui dtient un verrou est retard cause dune opration dE/S bloquante, dune faute de page ou de tout autre dlai, il est possible quaucun autre thread ne puisse plus progresser. Un algorithme est dit non bloquant si lchec ou la suspension de nimporte quel thread ne peut pas causer lchec ou la suspension dun autre ; un algorithme est dit sans verrouillage si, chaque instant, un thread peut progresser. Sils sont crits correctement, les algorithmes qui synchronisent les threads en utilisant exclusivement CAS peuvent tre la fois non bloquants et sans verrouillage. Une instruction CAS non dispute russit toujours et, si plusieurs threads luttent pour elle, il y en aura toujours un qui gagnera et pourra donc progresser. Les algorithmes non

Chapitre 15

Variables atomiques et synchronisation non bloquante

337

bloquants sont galement immuniss contre les interblocages ou linversion de priorit (bien quils puissent provoquer une famine ou un livelock car ils impliquent des essais rptition). Nous avons dj tudi un algorithme non bloquant : CasCounter. Il existe de bons algorithmes non bloquants connus pour de nombreuses structures de donnes classiques comme les piles, les les, les les priorits et les tables de hachage leur conception est une tche quil est prfrable de laisser aux experts. 15.4.1 Pile non bloquante Les algorithmes non bloquants sont considrablement plus compliqus que leurs quivalents avec verrous. La cl de leur conception consiste trouver comment limiter la porte des modications atomiques une variable unique tout en maintenant la cohrence des donnes. Avec les classes collections comme les les, on peut parfois sen tirer en exprimant les transformations de ltat en termes de modications des diffrents liens et en utilisant un objet AtomicReference pour reprsenter chaque lien devant tre modi de faon atomique. Les piles sont les structures de donnes chanes les plus simples : chaque lment nest li qu un seul autre et chaque lment nest dsign que par une seule rfrence. La classe ConcurrentStack du Listing 15.6 montre comment construire une pile laide de rfrences atomiques. La pile est une liste chane dlments Node partant de top, chacun deux contient une valeur et un lien vers llment suivant. La mthode push() prpare un nouveau Node dont le champ next pointe vers le sommet actuel de la pile (dsign par top), puis utilise CAS pour essayer de linstaller au sommet de la pile. Si le nud au sommet de la pile est le mme quau dbut de la mthode, linstruction CAS russit ; si ce nud a chang (parce quun autre thread a ajout ou t des lments depuis le lancement de la mthode), CAS choue : push() met jour le nouveau nud en fonction de ltat actuel de la pile puis ressaie. Dans les deux cas, la pile est toujours dans un tat cohrent aprs lexcution de CAS.
Listing 15.6 : Pile non bloquante utilisant lalgorithme de Treiber (Treiber, 1986).
@ThreadSafe public class ConcurrentStack <E> { AtomicReference <Node<E>> top = new AtomicReference <Node<E>>(); public void push(E item) { Node<E> newHead = new Node<E>(item); Node<E> oldHead; do { oldHead = top.get(); newHead.next = oldHead; } while (!top.compareAndSet(oldHead, newHead)); } public E pop() { Node<E> oldHead; Node<E> newHead; do {

338

Sujets avancs

Partie IV

Listing 15.6 : Pile non bloquante utilisant lalgorithme de Treiber (Treiber, 1986). (suite)
oldHead = top.get(); if (oldHead == null) return null; newHead = oldHead.next; } while (!top.compareAndSet(oldHead, newHead)); return oldHead.item; } private static class Node <E> { public final E item; public Node<E> next; public Node(E item) { this.item = item; } } }

CasCounter et ConcurrentStack illustrent les caractristiques de tous les algorithmes non bloquants : une partie du traitement repose sur des suppositions et peut devoir tre refait. Lorsque lon construit lobjet Node reprsentant le nouvel lment dans Concurrent Stack, on espre que la valeur de la rfrence next sera correcte lorsquon linstallera sur la pile, mais on est prt ressayer en cas de comptition.

La scurit vis--vis des threads des algorithmes non bloquants comme Concurrent Stack provient du fait que, comme le verrouillage, compareAndSet() fournit la fois des garanties datomicit et de visibilit. Lorsquun thread modie ltat de la pile, il le fait par un appel compareAndSet(), qui a les effets mmoire dune criture volatile. Un thread voulant examiner la pile appellera get() sur le mme objet AtomicReference, ce qui aura les effets mmoire dune lecture volatile. Toutes les modications effectues par un thread sont donc publies correctement vers tout autre thread qui examine ltat. La liste est modie par un appel compareAndSet() qui, de faon atomique, change la rfrence vers le sommet ou choue si elle dtecte une interfrence avec un autre thread. 15.4.2 File chane non bloquante Les deux algorithmes non bloquants que nous venons dtudier, le compteur et la pile, illustrent le modle dutilisation classique de linstruction CAS pour modier une valeur de faon spculative, en ressayant en cas dchec. Lastuce pour construire des algorithmes non bloquants consiste limiter la porte des modications atomiques une seule variable ; cest videmment trs simple pour les compteurs et assez facile pour les piles. En revanche, cela peut devenir bien plus compliqu pour les structures de donnes complexes, comme les les, les tables de hachage ou les arbres. Une le chane est plus complexe quune pile puisquelle doit permettre un accs rapide la fois sa tte et sa queue. Pour ce faire, elle gre deux pointeurs distincts, lun vers sa tte, lautre vers sa queue. Il y a donc deux pointeurs qui dsignent la queue dune le : le pointeur next de lavant-dernier lment et le pointeur vers la queue. Pour insrer correctement un nouvel lment, ces deux pointeurs doivent donc tre modis

Chapitre 15

Variables atomiques et synchronisation non bloquante

339

simultanment, de faon atomique. Au premier abord, ceci ne peut pas tre ralis laide de variables atomiques ; on a besoin doprations CAS distinctes pour modier les deux pointeurs et, si la premire russit alors que la seconde choue, la le sera dans un tat incohrent. Mme si elles russissaient toutes les deux, un autre thread pourrait tenter daccder la le entre le premier et le deuxime appel. La construction dun algorithme non bloquant pour une le chane ncessite donc de prvoir ces deux situations.
Figure 15.3
File avec deux lments, dans un tat stable.

queue tte

sentinelle

Nous avons pour cela besoin de plusieurs astuces. La premire consiste sassurer que la structure de donnes est toujours dans un tat cohrent, mme au milieu dune mise jour forme de plusieurs tapes. De cette faon, si le thread A est au milieu dune mise jour lorsque le thread B arrive, B peut savoir quune opration est en cours et quil ne doit pas essayer immdiatement dappliquer ses propres modications. B peut alors attendre (en examinant rgulirement ltat de la le) que A nisse an que chacun deux ne se mette pas en travers du chemin de lautre. Bien que cette astuce sufse pour que les threads "prennent des dtours" an daccder la structure de donnes sans la perturber, aucun thread ne pourra accder la le si un thread choue au milieu dune modication. Pour que lalgorithme soit non bloquant, il faut donc garantir que lchec dun thread nempchera pas les autres de progresser. La seconde astuce consiste donc sassurer que, si B arrive au milieu dune mise jour de A, la structure de donnes contiendra sufsamment dinformations pour que B nisse la modication de A. Si B "aide" A en nissant lopration commence par ce dernier, B pourra continuer la sienne sans devoir attendre A. Lorsque A reviendra pour nir son traitement, il constatera que B a dj fait le travail pour lui. La classe LinkedQueue du Listing 15.7 montre la partie consacre linsertion dans lalgorithme non bloquant de Michael-Scott (Michael et Scott, 1996) pour les les chanes, qui est celui utilis par ConcurrentLinkedQueue. Comme dans de nombreux algorithmes sur les les, une le vide est forme dun nud "sentinelle" ou "bidon" et les pointeurs de tte et de queue pointent initialement vers cette sentinelle. Le pointeur de queue dsigne toujours la sentinelle (si la le est vide), le dernier lment ou (lorsquune opration est en cours) lavant-dernier. La Figure 15.3 montre une le avec deux lments, dans un tat normal ou stable. Linsertion dun nouvel lment implique deux modications de pointeurs. La premire lie le nouveau nud la n de la liste en modiant le pointeur next du dernier

340

Sujets avancs

Partie IV

lment courant ; la seconde dplace le pointeur de queue pour quil dsigne le nouvel lment ajout. Entre ces deux oprations, la le est dans ltat intermdiaire reprsent la Figure 15.4. Aprs la seconde modication, elle est nouveau dans ltat stable de la Figure 15.5.
queue tte

sentinelle Figure 15.4

File dans un tat intermdiaire pendant linsertion.

queue tte

sentinelle Figure 15.5

File nouveau dans un tat stable aprs linsertion.

Le point essentiel qui permet ces deux astuces de fonctionner est que, si la le est dans un tat stable, le champ next du nud point par tail vaut null alors quil est diffrent de null si elle est dans un tat intermdiaire. Un thread peut donc immdiatement connatre ltat de la le en consultant tail.next. En outre, lorsquelle est dans un tat intermdiaire, la le peut tre restaure dans un tat stable en avanant le pointeur tail dun nud vers lavant, ce qui termine linsertion dun lment pour un thread qui se trouve au milieu de lopration1.
Listing 15.7 : Insertion dans lalgorithme non bloquant de Michael-Scott (Michael et Scott, 1996).
@ThreadSafe public class LinkedQueue <E> { private static class Node <E> { final E item; final AtomicReference <Node<E>> next;

1. Pour une tude complte de cet algorithme, voir (Michael et Scott, 1996) ou (Herlihy et Shavit, 2006).

Chapitre 15

Variables atomiques et synchronisation non bloquante

341

public Node(E item, Node<E> next) { this.item = item; this.next = new AtomicReference <Node<E>>(next); } } private final Node<E> sentinelle = new Node<E>(null, null); private final AtomicReference <Node<E>> head = new AtomicReference <Node<E>>(sentinelle); private final AtomicReference <Node<E>> tail = new AtomicReference <Node<E>>(sentinelle); public boolean put(E item) { Node<E> newNode = new Node<E>(item, null); while (true) { Node<E> curTail = tail.get(); Node<E> tailNext = curTail.next.get(); if (curTail == tail.get()) { if (tailNext != null) { // La file est dans un tat intermdiaire, on avance tail tail.compareAndSet(curTail, tailNext); } else { // Dans ltat stable, on tente dinsrer le nouveau noeud if (curTail.next.compareAndSet(null, newNode)) { // Insertion russie, on tente davancer tail tail.compareAndSet(curTail, newNode); return true; } } } } } }

A B

C D

Avant de tenter dinsrer un nouvel lment, LinkedQueue.put() teste dabord si la le est dans ltat intermdiaire (tape A). Si cest le cas, cela signie quun autre thread est dj en train dinsrer un lment (il est entre les tapes C et D). Au lieu dattendre quil nisse, le thread courant laide en nissant lopration pour lui, ce qui consiste avancer le pointeur tail (tape B). Puis il revrie au cas o un autre thread aurait commenc ajouter un nouvel lment, en avanant le pointeur tail jusqu trouver la le dans un tat stable, an de pouvoir commencer sa propre insertion. Linstruction CAS de ltape C, qui lie le nouveau nud la queue de la le, pourrait chouer si deux threads tentent dajouter un lment simultanment. Dans ce cas, il ny aura aucun problme : aucune modication na t faite et il suft que le thread courant recharge le pointeur tail et ressaie. Lorsque C a russi, linsertion est considre comme effectue ; la seconde instruction CAS (tape D) est du "nettoyage" puisquelle peut tre effectue soit par le thread qui insre, soit par un autre. Si elle choue, le thread qui insre repart plutt que retenter dexcuter CAS : il ny a pas besoin de ressayer puisquun autre thread a dj ni le travail lors de son tape B ! Ceci fonctionne parce quavant de tenter de lier un nouveau nud dans la le tout thread vrie que la le ncessite un nettoyage en testant que tail.next ne vaut pas null. Dans ce cas, il avance dabord le pointeur tail (ventuellement plusieurs fois) jusqu ce que la le soit dans ltat stable.

342

Sujets avancs

Partie IV

15.4.3 Modicateurs atomiques de champs Bien que le Listing 15.7 illustre lalgorithme utilis par ConcurrentLinkedQueue, la vritable implmentation est un peu diffrente. Comme le montre le Listing 15.8, au lieu de reprsenter chaque Node par une rfrence atomique, ConcurrentLinkedQueue utilise une rfrence volatile ordinaire et la met jour grce la classe AtomicReferenceField Updater, qui utilise lintrospection.
Listing 15.8 : Utilisation de modicateurs atomiques de champs dans ConcurrentLinkedQueue.
private class Node<E> { private final E item; private volatile Node<E> next; public Node(E item) { this.item = item; } } private static AtomicReferenceFieldUpdater <Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next");

Les classes modicateurs atomiques de champs (disponibles en versions Integer, Long et Reference) prsentent une vue "introspective" dun champ volatile an que CAS puisse tre utilise sur des champs volatiles. Elles nont pas de constructeur : pour crer un modicateur, il faut appeler la mthode fabrique newUpdater(). Ces classes ne sont pas lies une instance spcique ; on peut utiliser un modicateur pour mettre jour le champ de nimporte quelle instance de la classe cible. Les garanties datomicit pour les classes modicateurs sont plus faibles que pour les classes atomiques normales car on ne peut pas garantir que les champs sous-jacents ne seront pas modis directement les mthodes compareAndSet() et arithmtiques ne garantissent latomicit que par rapport aux autres threads qui utilisent les mthodes du modicateur atomique de champ. Dans ConcurrentLinkedQueue, les mises jour du champ next dun Node sont appliques laide de la mthode compareAndSet() de nextUpdater. On utilise cette approche quelque peu tortueuse uniquement pour des raisons de performances. Pour les objets souvent allous, ayant une dure de vie courte (comme les nuds dune liste chane), llimination de la cration dun objet AtomicReference pour Node permet de rduire signicativement le cot des oprations dinsertion. Toutefois, dans quasiment tous les cas, les variables atomiques ordinaires sufsent les modicateurs atomiques ne sont ncessaires que dans quelques situations (ils sont galement utiles pour effectuer des mises jour atomiques tout en prservant la forme srialise dune classe existante). 15.4.4 Le problme ABA Le problme ABA est une anomalie pouvant rsulter dune utilisation nave de compareret-changer dans des algorithmes o les nuds peuvent tre recycls (essentiellement dans des environnements sans ramasse-miettes). En ralit, une opration CAS pose la

Chapitre 15

Variables atomiques et synchronisation non bloquante

343

question "est-ce que la valeur de V est toujours A ?" et effectue la mise jour si cest le cas. Dans la plupart des situations, dont celles des exemples de ce chapitre, cest entirement sufsant. Cependant, on veut parfois plutt demander "est-ce que la valeur de V a t modie depuis que jai constat quelle valait A ?". Pour certains algorithmes, changer la valeur de V de A en B puis en A compte quand mme comme une modication qui ncessite de ressayer une tape de lalgorithme. Ce problme ABA peut se poser dans les algorithmes qui effectuent leur propre gestion mmoire pour les nuds de la liste chane. Dans ce cas, que la tte dune liste dsigne toujours un nud dj observ ne suft pas impliquer que le contenu de cette liste na pas t modi. Si vous ne pouvez pas viter le problme ABA en laissant le ramassemiettes grer les nuds pour vous, il reste une solution relativement simple : au lieu de modier la valeur dune rfrence, modiez deux valeurs une rfrence et un numro de version. Mme si la valeur passe de A B, puis de nouveau A, les numros de version seront diffrents. AtomicStampedReference et sa cousine AtomicMarkable Reference fournissent une mise jour atomique conditionnelle de deux variables. AtomicStampedReference modie une paire rfrence dobjet-entier, ce qui permet davoir des rfrences "avec numros de versions", immunises1 contre le problme ABA. De mme, AtomicMarkableReference modie une paire rfrence dobjet-boolen, ce qui permet certains algorithmes de laisser un nud dans une liste bien quil soit marqu comme supprim2.

Rsum
Les algorithmes non bloquants grent la scurit par rapport aux threads en utilisant des primitives de bas niveau, comme comparer-et-changer, la place des verrous. Ces primitives sont exposes via les classes de variables atomiques, qui peuvent galement servir de "meilleures variables volatiles" en fournissant des oprations de mises jour atomiques pour les entiers et les rfrences dobjets. Ces algorithmes sont difciles concevoir et implmenter, mais peuvent offrir une meilleure adaptabilit dans des situations typiques et une plus grande rsistance contre les checs de vivacit. La plupart des avances dune version lautre de la JVM en termes de performances de la concurrence proviennent de lutilisation de ces algorithmes, la fois dans la JVM et dans la bibliothque standard.

1. En pratique, en tout cas... car, thoriquement, le compteur pourrait reboucler. 2. De nombreux processeurs disposent dune instruction CAS double (CAS2 ou CASX) qui peut porter sur une paire pointeur-entier, ce qui rendrait cette opration assez efcace. partir de Java 6, AtomicStampedReference nutilise pas ce CAS double, mme sur les plates-formes qui en disposent (le CAS double est diffrent de DCAS, qui agit sur deux emplacements mmoire distincts cette dernire nexiste actuellement sur aucun processeur).

16
Le modle mmoire de Java
Tout au long de ce livre, nous avons vit les dtails de bas niveau du modle mmoire de Java (MMJ) pour nous intresser aux problmes de conception qui se situent un niveau suprieur, comme la publication correcte et la spcication et la conformit aux politiques de synchronisation. Pour utiliser efcacement tous ces mcanismes, il peut tre intressant de comprendre comment ils fonctionnent, sachant que toutes leurs fonctionnalits prennent racine dans le MMJ. Ce chapitre lve donc le voile sur les exigences et les garanties de bas niveau du modle mmoire de Java et rvle le raisonnement qui se cache derrire certaines des rgles de conception que nous avons utilises dans cet ouvrage.

16.1

Quest-ce quun modle mmoire et pourquoi en a-t-on besoin ?

Supposons quun thread affecte une valeur la variable aVariable :


aVariable = 3;

Un modle mmoire rpond la question suivante : "Sous quelles conditions un thread qui lit aVariable verra la valeur 3 ? " Cette question peut sembler un peu idiote mais, en labsence de synchronisation, il existe un certain nombre de raisons pour lesquelles un thread pourrait ne pas voir immdiatement (ou ne jamais voir) le rsultat dune opration effectue par un autre thread. Les compilateurs peuvent, en effet, produire des instructions dans un ordre qui est diffrent de celui "vident" suggr par le code source ou stocker des variables dans des registres au lieu de les mettre en mmoire ; les processeurs peuvent excuter des instructions en parallle ou dans nimporte quel ordre, les caches peuvent modier lordre dans lequel les critures dans les variables sont transmises la mmoire principale, et les valeurs stockes dans les caches locaux dun processeur peuvent ne pas tre visibles par les autres processeurs. Tous ces facteurs peuvent donc empcher un thread de voir la dernire valeur en date dune variable et provoquer des

346

Sujets avancs

Partie IV

actions mmoire dans dautres threads qui sembleront totalement mlanges si lon nutilise pas une synchronisation adquate. Dans un contexte monothread, tous les tours que joue lenvironnement votre programme sont cachs et nont dautres effets que dacclrer son excution. La spcication du langage Java exige que la JVM ait une smantique apparemment squentielle au sein dun thread : tant que le programme a le mme rsultat que sil tait excut dans un environnement strictement squentiel, toutes les astuces sont permises. Cest une bonne chose car ces rarrangements sont responsables de lessentiel des amliorations des performances de calcul de ces dernires annes. Laccroissement des frquences dhorloge a certainement contribu augmenter les performances, mais le paralllisme accru les units dexcution superscalaires en pipelines, la planication dynamique des instructions, lexcution spculative et les caches mmoire multiniveaux y a galement sa part. mesure que les processeurs deviennent plus sophistiqus, les compilateurs doivent aussi voluer en rarrangeant les instructions pour faciliter une excution optimale et en utilisant des algorithmes volus dallocation des registres. Les constructeurs de processeurs se tournant dsormais vers les processeurs multicurs, en grande partie parce quil devient difcile daugmenter encore les frquences dhorloge, le paralllisme matriel ne fera quaugmenter. Dans un environnement multithread, lillusion de la squentialit ne peut tre entretenue sans que cela ait un cot signicatif pour les performances. La plupart du temps, les threads dune application concurrente font, chacun, "leurs propres affaires" et une synchronisation excessive entre eux ne ferait que ralentir lapplication. Ce nest que lorsque plusieurs threads partagent des donnes quil faut coordonner leurs activits, et la JVM se e au programme pour identier cette situation. Le MMJ prcise les garanties minimales que la JVM doit prendre sur le moment o les critures dans les variables deviennent visibles aux autres threads. Il a t conu pour quilibrer le besoin de prvisibilit et de facilit de dveloppement des programmes par rapport aux ralits de limplmentation de JVM performantes sur un grand nombre darchitectures processeurs connues. Cela tant dit, lorsque lon nest pas habitu aux astuces employes par les processeurs et les compilateurs modernes pour amliorer au maximum les performances des programmes, certains aspects du MMJ peuvent sembler troublants premire vue. 16.1.1 Modles mmoire des plates-formes Dans une architecture multiprocesseur mmoire partage, chaque processeur a son propre cache, qui est priodiquement synchronis avec la mmoire principale. Les architectures des processeurs fournissent diffrents degrs de cohrence des caches ; certaines offrent des garanties minimales qui autorisent plusieurs processeurs voir, quasiment en permanence, des valeurs diffrentes pour le mme emplacement mmoire. Le systme dexploitation, le compilateur et lenvironnement dexcution (et parfois galement le

Chapitre 16

Le modle mmoire de Java

347

programme) doivent combler les diffrences entre ce qui est fourni par le matriel et ce quexige la scurit par rapport aux threads. Sassurer que chaque processeur sache ce que font en permanence les autres est une opration coteuse. La plupart du temps, cette information ntant pas ncessaire, les processeurs assouplissent leurs garanties sur la cohrence mmoire an damliorer les performances. Un modle mmoire darchitecture indique aux programmes les garanties quils peuvent attendre du systme de mmoire et prcise les instructions spciales (barrires mmoires) qui sont ncessaires pour disposer de garanties supplmentaires de coordination de la mmoire lors du partage des donnes. Pour protger le dveloppeur Java des diffrences existant entre ces modles mmoire en fonction des architectures, Java fournit son propre modle mmoire et la JVM traite les diffrences entre le MMJ et le modle mmoire de la plate-forme sous-jacente en insrant des barrires mmoires aux endroits adquats. Une vision pratique de lexcution dun programme consiste imaginer que ses oprations ne peuvent se drouler que dans un seul ordre, quel que soit le processeur sur lequel il sexcute, et que chaque lecture de variable verra la dernire valeur qui y a t crite par nimporte quel processeur. Ce modle idal, bien quirraliste, est appel cohrence squentielle. Les dveloppeurs la supposent souvent tort car aucun systme multiprocesseur actuel noffre la cohrence squentielle, pas plus que le MMJ. Le modle de traitement squentiel classique, celui de Von Neumann, nest quune vague approximation du comportement des multiprocesseurs modernes. Il en rsulte que les systmes multiprocesseurs mmoire partage actuels (et les compilateurs) peuvent faire des choses surprenantes lorsque des donnes sont partages entre les threads, sauf si on leur prcise de ne pas utiliser directement les barrires mmoires. Heureusement, les programmes Java nont pas besoin dindiquer les emplacements de ces barrires : ils doivent simplement savoir quels moments on accde ltat partag et utiliser correctement la synchronisation. 16.1.2 Rorganisation Lorsque nous avons dcrit les situations de comptition (data race) et les checs datomicit au Chapitre 2, nous avons utilis des diagrammes dactions dcrivant les "timings malheureux", o lentrelacement des oprations produisait des rsultats incorrects dans les programmes mal synchroniss. Mais il y a pire : le MMJ peut permettre des actions de sembler sexcuter dans un ordre diffrent en fonction des diffrents threads, ce qui complique dautant plus le raisonnement sur leur enchanement en labsence de synchronisation. Les diverses raisons pour lesquelles les oprations pourraient tre retardes ou sembler sexcuter dans nimporte quel ordre peuvent toutes tre rassembles dans la catgorie gnrale de la rorganisation.

348

Sujets avancs

Partie IV

La classe PossibleReordering du Listing 16.1 illustre la difcult de comprendre les programmes concurrents les plus simples lorsquils ne sont pas correctement synchroniss. Il est assez facile dimaginer comment PossibleReordering pourrait afcher (1, 0), (0, 1) ou (1, 1) : le thread A pourrait sexcuter jusqu la n avant que le thread B ne commence, B pourrait sexcuter jusqu la n avant que A ne dmarre ou leurs actions pourraient sentrelacer. Mais, trangement, PossibleReordering peut galement afcher (0, 0) ! Les actions de chaque thread nayant aucune dpendance entre elles peuvent donc sexcuter dans nimporte quel ordre (mme si elles taient excutes dans lordre, le timing avec lequel les caches sont vids en mmoire peut faire, du point de vue de B, que les affectations qui ont lieu dans A apparaissent dans lordre oppos). La Figure 16.1 montre un entrelacement possible avec rorganisation qui produit lafchage de (0, 0).
Listing 16.1 : Programme mal synchronis pouvant produire des rsultats surprenants. Ne le faites pas.
public class PossibleReordering { static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start(); other.start(); one.join(); other.join(); System.out.println("( "+ x + "," + y + ")"); } }

Figure 16.1
Entrelacement montrant une rorganisation dans PossibleReordering.

ThreadA ThreadB

x=b (0)

rorganisation

a=1

b=1

y=a (0)

Bien que PossibleReordering soit un programme trivial, il est tonnamment difcile dnumrer tous ses rsultats possibles. La rorganisation au niveau de la mmoire peut provoquer des comportements inattendus des programmes, et il est excessivement difcile de comprendre cette rorganisation en labsence de synchronisation ; il est bien plus facile de sassurer que le programme est correctement synchronis. La synchronisation empche le compilateur, lenvironnement dexcution et le matriel de rorganiser

Chapitre 16

Le modle mmoire de Java

349

les oprations mmoire dune faon qui violerait les garanties de visibilit assures par le MMJ1. 16.1.3 Le modle mmoire de Java en moins de cinq cents mots Le modle mmoire de Java est spci en termes dactions, qui incluent les lectures et les critures de variables, les verrouillages et dverrouillages des moniteurs et le lancement et lattente des threads. Le MMJ dnit un ordre partiel2 appel arrive-avant sur toutes les actions dun programme. Pour garantir que le thread qui excute laction B puisse voir le rsultat de laction A (que A et B sexcutent ou non dans des threads diffrents), il doit exister une relation arrive-avant entre A et B. En son absence, la JVM est libre de les rorganiser selon son bon vouloir. 3 4 Une situation de comptition (data race) intervient lorsquune variable est lue par plusieurs threads et crite par au moins un thread alors que les lectures et les critures ne sont pas ordonnes par arrive-avant. Un programme correctement synchronis na pas de situation de comptition et prsente une cohrence squentielle, ce qui signie que toutes les actions du programme sembleront sexcuter dans un ordre xe et global.
Les rgles de arrive-avant sont : Rgle de lordre des programmes. Toute action dun thread arrive-avant toute autre action de ce thread place aprs dans lordre du programme. Rgle des verrous de moniteurs. Le dverrouillage dun verrou de moniteur arriveavant tout verrouillage ultrieur sur ce mme verrou3. Rgle des variables volatiles. Une criture dans un champ volatile arrive-avant toute lecture de ce champ4. Rgle de lancement des threads. Un appel Thread.start() sur un thread arriveavant toute action dans le thread lanc. Rgle de terminaison des threads. Toute action dun thread arrive-avant quun autre thread ne dtecte que ce thread sest termin, soit en revenant avec succs de Thread.join, soit par un appel Thread.isAlive() qui renvoie false.

1. Sur la plupart des architectures de processeurs connues, le modle mmoire est sufsamment fort pour que le cot en termes de performances dune lecture volatile soit quivalent celui dune lecture non volatile. 2. Un ordre partiel < est une relation antisymtrique, rexive et transitive sur un ensemble, mais deux lments quelconques x et y ne doivent pas ncessairement vrier x < y ou y < x. On utilise tous les jours un ordre partiel pour exprimer nos prfrences : on peut prfrer les sushis aux cheeseburgers et Mozart Mahler, bien que nous nayions pas ncessairement une prfrence marque entre les cheeseburgers et Mozart. 3. Les verrouillages et dverrouillages des objets Lock explicites ont la mme smantique mmoire que les verrous internes. 4. Les lectures et critures des variables atomiques ont la mme smantique mmoire que les variables volatiles.

350

Sujets avancs

Partie IV

Rgle des interruptions. Un thread appelant interrupt() sur un autre thread arriveavant que le thread interrompu ne dtecte linterruption (soit par le lancement de lexception InterruptedException, soit en appelant isInterrupted() ou interrupted()). Rgle des nalisateurs. La n dun constructeur dobjet arrive-avant le lancement de son nalisateur. Transitivit. Si A arrive-avant B et que B arrive-avant C, alors, A arrive-avant C.

Bien que les actions ne soient que partiellement ordonnes, les actions de synchronisation acquisition et libration des verrous, lectures et critures des variables volatiles le sont totalement. On peut donc dcrire arrive-avant en termes de prises de verrous et de lectures de variables volatiles "ultrieures". La Figure 16.2 illustre la relation arrive-avant lorsque deux threads se synchronisent laide dun verrou commun. Toutes les actions dans le thread A sont ordonnes par le programme selon la rgle de lordre des programmes, tout comme les actions du thread B.

Thread A y=1

verrouille M

x=1 Tout ce qui a eu lieu avant le dverrouillage de M... ... est visible tout ce qui est aprs le verrouillage de M Thread B

dverrouille M

verrouille M

i=x

dverrouille M

j=y

Figure 16.2
Illustration de arrive-avant dans le modle mmoire de Java.

Chapitre 16

Le modle mmoire de Java

351

Le thread A relchant le verrou M et B le prenant ensuite, toutes les actions de A avant sa libration du verrou sont donc ordonnes avant les actions de B aprs quil a pris le verrou. Lorsque deux threads se synchronisent sur des verrous diffrents, on ne peut rien dire de lordre des actions entre eux il ny a aucune relation arrive-avant entre les actions des deux threads. 16.1.4 Tirer parti de la synchronisation Grce la force de lordre arrive-avant, on peut parfois proter des proprits de la visibilit dune synchronisation existante. Ceci suppose de combiner la rgle de lordre des programmes de arrive-avant avec une de ses autres rgles (gnralement celle concernant le verrouillage des moniteurs ou celle des variables volatiles) an dordonner les accs une variable qui nest pas protge par un verrou. Cette approche est trs sensible lordre dapparition des instructions et est donc assez fragile ; il sagit dune technique avance qui devrait tre rserve lextraction de la dernire goutte de performance des classes comme ReentrantLock. Limplmentation des mthodes protges de AbstractQueuedSynchronizer dans Future Task illustre ce mcanisme. AQS gre un entier de ltat du synchronisateur que FutureTask utilise pour stocker ltat de la tche : en cours dexcution, termine ou annule. Mais FutureTask gre galement dautres variables, comme le rsultat de son calcul. Lorsquun thread appelle set() pour sauvegarder le rsultat et quun autre thread appelle get() pour le rcuprer, il vaut mieux quils soient tous les deux ordonns selon arrive-avant. Pour ce faire, on pourrait rendre volatile la rfrence au rsultat, mais il est possible dexploiter la synchronisation existante pour obtenir le mme effet moindre cot.
FutureTask a t soigneusement conue pour garantir quun appel russi try ReleaseShared() arrive-avant un appel ultrieur tryAcquireShared() ; en effet, tryReleaseShared() crit toujours dans une variable volatile qui est lue par tryAcquire Shared(). Le Listing 16.2 prsente les mthodes innerSet() et innerGet(), qui sont appeles lorsque le rsultat est sauvegard ou relu ; comme innerSet() crit le rsultat avant dappeler releaseShared() (qui appelle tryReleaseShared()) et que innerGet() lit le rsultat aprs avoir appel acquireShared() (qui appelle tryAcquireShared()), la rgle de lordre des programmes se combine avec celle des variables volatiles pour garantir que lcriture du rsultat dans innerSet() arrive-avant sa lecture dans innerGet().
Listing 16.2 : Classe interne de FutureTask illustrant une mise prot de la synchronisation.
// Classe interne de FutureTask private final class Sync extends AbstractQueuedSynchronizer { private static final int RUNNING = 1, RAN = 2, CANCELLED = 4; private V result; private Exception exception; void innerSet(V v) { while (true) {

352

Sujets avancs

Partie IV

Listing 16.2 : Classe interne de FutureTask illustrant une mise prot de la synchronisation. (suite)
int s = getState(); if (ranOrCancelled(s)) return; if (compareAndSetState (s, RAN)) break; } result = v; releaseShared(0); done(); } V innerGet() throws InterruptedException , ExecutionException { acquireSharedInterruptibly(0); if (getState() == CANCELLED) throw new CancellationException (); if (exception != null) throw new ExecutionException (exception); return result; } }

On appelle cette technique piggybacking car elle utilise un ordre arrive-avant existant qui a t cr pour garantir la visibilit de lobjet X au lieu de crer cet ordre uniquement pour publier X. Le piggybacking comme celui utilis par FutureTask est assez fragile et ne devrait pas tre utilis inconsidrment. Cependant, cest une technique parfaitement justiable quand, par exemple, une classe sen remet un ordre arrive-avant entre les mthodes car cela fait partie de sa spcication. Une publication correcte utilisant une BlockingQueue, par exemple, est une forme de piggybacking. Un thread plaant un objet dans une le et un autre le rcuprant ultrieurement constituent une publication correcte car on est sr quil y a une synchronisation interne sufsante dans limplmentation de BlockingQueue pour garantir que lajout arrive-avant la suppression. Les autres ordres arrive-avant garantis par la bibliothque standard incluent les suivants :
m

Le placement dun lment dans une collection thread-safe arrive-avant quun autre thread ne rcupre cet lment partir de la collection. Le dcomptage sur un CountDownLatch arrive-avant quun thread sorte de await() sur ce loquet. La libration dun permis de Semaphore arrive-avant dacqurir un permis de ce mme smaphore. Les actions entreprises par la tche reprsente par un objet Future arrivent-avant quun autre thread revienne avec succs dun appel Future.get(). La soumission dun Runnable ou dun Callable un Executor arrive-avant que la tche ne commence son excution.

Chapitre 16

Le modle mmoire de Java

353

Un thread arrivant sur un objet CyclicBarrier ou Exchanger arrive-avant que les autres threads ne soient librs de cette barrire ou de ce point dchange. Si Cyclic Barrier utilise une action de barrire, larrive la barrire arrive-avant laction de barrire, qui, elle-mme, arrive-avant que les threads ne soient librs de la barrire.

16.2

Publication

Le Chapitre 3 a montr comment un objet pouvait tre publi correctement ou incorrectement. La thread safety des techniques de publication correctes dcrites ici provient des garanties fournies par le MMJ ; les risques de publication incorrectes sont des consquences de labsence dordre arrive-avant entre la publication dun objet partag et son accs partir dun autre thread. 16.2.1 Publication incorrecte Lventualit dune rorganisation en labsence de relation arrive-avant explique pourquoi la publication dun objet sans synchronisation approprie peut faire quun autre thread voit un objet partiellement construit (voir la section 3.5). Linitialisation dun nouvel objet implique dcrire dans des variables les champs du nouvel lobjet. De mme, la publication dune rfrence implique dcrire dans une autre variable la rfrence au nouvel objet. Si vous ne vous assurez pas que la publication de la rfrence partage arrive-avant quun autre thread la charge, lcriture de cette rfrence au nouvel objet peut tre rorganise (du point de vue du thread qui consomme lobjet) et se mlanger aux critures dans ses champs. En ce cas, un autre thread pourrait voir une valeur jour pour la rfrence lobjet mais des valeurs obsoltes pour une partie de ltat (voire pour tout ltat) de cet objet il verrait donc un objet partiellement construit. La publication incorrecte peut tre le rsultat dune initialisation paresseuse incorrecte, comme celle du Listing 16.3. Au premier abord, le seul problme ici semble tre la situation de comptition dcrite dans la section 2.2.2. Dans certaines circonstances lorsque toutes les instances de Resource sont identiques, par exemple , on peut vouloir ne pas en tenir compte ; malheureusement, mme si ces dfauts sont ignors, UnsafeLazyInitialization nest quand mme pas thread-safe car un autre thread pourrait voir une rfrence un objet Resource partiellement construit.
Listing 16.3 : Initialisation paresseuse incorrecte. Ne le faites pas.
@NotThreadSafe public class UnsafeLazyInitialization { private static Resource resource; public static Resource getInstance() { if (resource == null) resource = new Resource(); // publication incorrecte return resource; } }

354

Sujets avancs

Partie IV

Supposons que le thread A invoque en premier getInstance(). Il constate que resource vaut null, cre une nouvelle instance de Resource, quil dsigne par resource. Lorsque B appelle ensuite getInstance(), il peut constater que cette resource a dj une valeur non null et utilise simplement la Resource dj construite. premire vue, tout cela peut sembler sans danger mais il ny a pas dordre arrive-avant entre lcriture de resource dans A et sa lecture dans B : on a utilis une situation de comptition dans A pour publier lobjet, et B ne peut donc pas tre certain de voir ltat correct de la Resource. Le constructeur Resource modie les champs de lobjet Resource qui vient dtre allou pour quils passent de leurs valeurs par dfaut (crits par le constructeur de Object) leurs valeurs initiales. Comme aucun thread nutilise de synchronisation, B pourrait ventuellement voir les actions de A dans un ordre diffrent de celui utilis par ce dernier. Par consquent, mme si A a initialis lobjet Resource avant daffecter sa rfrence resource, B pourrait voir lcriture dans resource avant lcriture des champs de lobjet Resource et donc voir un objet partiellement construit qui peut trs bien tre dans un tat incohrent et dont ltat peut changer plus tard de faon inattendue.
Lutilisation dun objet modiable initialis dans un autre thread nest pas sre, sauf si la publication arrive-avant que le thread consommateur ne lutilise.

16.2.2 Publication correcte Les idiomes de publication correcte dcrits au Chapitre 3 garantissent que lobjet publi est visible aux autres threads car ils assurent que la publication arrive-avant quun thread consommateur ne charge une rfrence lobjet publi. Si le thread A place X dans une BlockingQueue (et quaucun thread ne le modie ensuite) et que le thread B le rcupre, B est assur de voir X comme A la laiss. Cela est d au fait que les implmentations de BlockingQueue ont une synchronisation interne sufsante pour garantir que le put() arrive-avant le take(). De mme, lutilisation dune variable partage protge par un verrou ou dune variable volatile partage garantit que les lectures et les critures seront ordonnes selon arrive-avant. Cette garantie est, en ralit, une promesse de visibilit et elle est dordre plus fort que celle de la publication correcte. Lorsque X est correctement publi de A vers B, cette publication garantit la visibilit de ltat de X, mais pas celle des autres variables que A a pu modier. En revanche, si le placement de X par A dans une le arrive-avant que B ne rcupre X partir de cette le, B voit non seulement X dans ltat o A la laiss (en supposant que X nait pas t ensuite modi par A ou un autre thread) mais galement

Chapitre 16

Le modle mmoire de Java

355

tout ce qua fait A avant ce transfert (en supposant encore quil ny ait pas eu de modications ultrieures)1. Pourquoi insistions-nous si lourdement sur @GuardedBy et la publication correcte alors que le MMJ nous fournit dj arrive-avant, qui est plus puissant ? Parce que penser en terme de transmission de proprit et de publication dobjet convient mieux la plupart des conceptions de programmes que raisonner en terme de visibilit des diffrentes critures en mmoire. Lordre arrive-avant intervient au niveau des accs mmoire individuels il sagit dune sorte de "langage assembleur de la concurrence". La publication correcte, en revanche, agit un niveau plus proche de celui du programme. 16.2.3 Idiomes de publication correcte Sil est parfois raisonnable de diffrer linitialisation des objets coteux jusquau moment o lon en a vraiment besoin, on vient aussi de voir quune mauvaise utilisation de linitialisation paresseuse peut poser des problmes. UnsafeLazyInitialization peut tre corrige en synchronisant la mthode getInstance(), comme dans le Listing 16.4. Le code de cette mthode tant assez court (un test et un branchement prvisible), les performances de cette approche seront sufsantes si getInstance() nest pas constamment appele par de nombreux threads.
Listing 16.4 : Initialisation paresseuse thread-safe.
@ThreadSafe public class SafeLazyInitialization { private static Resource resource; public synchronized static Resource getInstance() { if (resource == null) resource = new Resource(); return resource; } }

Le traitement des champs statiques par les initialisateurs (ou des champs dont la valeur est xe dans un bloc dinitialisation statique [JPL 2.2.1 et 2.5.3]) est un peu spcial et offre des garanties de thread safety supplmentaires. Les initialisateurs statiques sont lancs par la JVM au moment de linitialisation de la classe, aprs son chargement mais avant quelle ne soit utilise par un thread. La JVM prenant un verrou au cours de linitialisation [JLS 12.4.2] et ce verrou tant pris au moins une fois par chaque thread pour garantir que la classe a t charge, les critures en mmoire au cours de linitialisation statique sont automatiquement visibles par tous les threads. Par consquent, les objets initialiss statiquement nexigent aucune synchronisation explicite au cours de leur construction ni lorsquils sont rfrencs. Cependant, ceci ne sapplique qu ltat au moment de la construction si lobjet est modiable, la synchronisation reste ncessaire
1. Le MMJ garantit que B voit une valeur au moins aussi jour que celle que A a crite ; les critures ultrieures peuvent tre visibles ou non.

356

Sujets avancs

Partie IV

pour les lecteurs et les crivains, an que les modications ultrieures soient visibles et pour viter de perturber les donnes.
Listing 16.5 : Initialisation impatiente.
@ThreadSafe public class EagerInitialization { private static Resource resource = new Resource(); public static Resource getResource() { return resource; } }

Lutilisation dune initialisation impatiente, prsente dans le Listing 16.5, limine le cot de la synchronisation subi par chaque appel getInstance() dans SafeLazy Initialization. Cette technique peut tre combine avec le chargement de classe paresseux de la JVM an de crer une technique dinitialisation paresseuse qui nexige pas de synchronisation du code commun. Lidiome de la classe conteneur dinitialisation paresseuse [EJ Item 48] prsent dans le Listing 16.6 utilise une classe dans le seul but dinitialiser lobjet Resource. La JVM diffre de linitialisation de la classe Resource Holder jusqu ce quelle soit vraiment utilise [JLS 12.4.1] et aucune synchronisation supplmentaire nest ncessaire puisque lobjet est initialis par un initialisateur statique. Le premier appel getResource() par nimporte quel thread provoque le chargement et linitialisation de ResourceHolder, et linitialisation de lobjet ressource a lieu au moyen de linitialisateur statique.
Listing 16.6 : Idiome de la classe conteneur de linitialisation paresseuse.
@ThreadSafe public class ResourceFactory { private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } }

16.2.4 Verrouillage contrl deux fois Aucun livre sur la concurrence ne serait complet sans une prsentation de linfme antipatron du verrouillage contrl deux fois (DCL, pour Double Checked Locking), prsent dans le Listing 16.7. Dans les toutes premires JVM, la synchronisation, mme sans comptition, avait un impact non ngligeable sur les performances. De nombreuses astuces ruses (ou, en tout cas, qui semblaient ltre) ont donc t inventes pour rduire ce cot certaines taient bonnes, certaines taient mauvaises et certaines taient horribles. DCL fait partie des "horribles". Les performances des premires JVM laissant dsirer, on utilisait souvent linitialisation paresseuse pour viter des oprations onreuses potentiellement inutiles ou pour rduire

Chapitre 16

Le modle mmoire de Java

357

le temps de dmarrage des applications. Une initialisation correctement crite exige une synchronisation. Or, lpoque, cette synchronisation tait lente et, surtout, pas totalement matrise : les aspects de lexclusion taient bien compris, mais ce ntait pas le cas de ceux lis la visibilit. DCL semblait alors offrir le meilleur des deux mondes une initialisation paresseuse qui ne pnalisait pas le code classique cause de la synchronisation. Cette astuce fonctionnait en testant dabord sans synchronisation sil y avait besoin dune initialisation et utilisait la rfrence de la ressource si elle ntait pas null. Sinon on synchronisait et on vriait nouveau si la ressource tait initialise an de sassurer quun seul thread puisse initialiser la ressource partage. Le code classique rcupration dune rfrence une ressource dj construite nutilisait pas de synchronisation. On se retrouvait donc avec le problme dcrit dans la section 16.2.1 : un thread pouvait voir une ressource partiellement construite. Le vritable problme de DCL est que cette technique suppose que le pire qui puisse arriver lorsquon lit une rfrence dobjet partag sans synchronisation est de voir par erreur une valeur obsolte (null, ici) ; en ce cas, lidiome DCL compense ce risque en ressayant avec le verrou. Cependant, le pire des cas est, en fait, encore pire on peut voir une valeur jour de la rfrence tout en voyant des valeurs obsoltes pour ltat de lobjet, ce qui signie que celui-ci peut tre vu dans un tat incorrect ou incohrent. Les modications apportes au MMJ (depuis Java 5.0) permettent DCL de fonctionner si la ressource est dclare volatile ; cependant, limpact sur les performances est minime puisque les lectures volatiles ne sont gnralement pas beaucoup plus coteuses que les lectures non volatiles.
Listing 16.7 : Antipatron du verrouillage vrifi deux fois. Ne le faites pas.
@NotThreadSafe public class DoubleCheckedLocking { private static Resource resource; public static Resource getInstance() { if (resource == null) { synchronized (DoubleCheckedLocking .class) { if (resource == null) resource = new Resource(); } } return resource; } }

Il sagit donc dun idiome aujourdhui totalement dpass les raisons qui lont motiv (synchronisation lente en cas dabsence de comptition, lenteur du lancement de la JVM) ne sont plus dactualit, ce qui le rend moins efcace en tant quoptimisation. Lidiome du conteneur dinitialisation paresseuse offre les mmes bnces et est plus simple comprendre.

358

Sujets avancs

Partie IV

16.3

Initialisation sre

La garantie dune initialisation sre permet aux objets non modiables correctement construits dtre partags en toute scurit sans synchronisation entre les threads, quelle que soit la faon dont ils sont publis mme si cette publication utilise une situation de comptition (ce qui signie que UnsafeLazyInitialization est sre si Resource nest pas modiable). Sans cette scurit dinitialisation, des objets censs tre non modiables, comme String, peuvent sembler changer de valeur si le thread qui publie et ceux qui consomment nutilisent pas de synchronisation. Larchitecture de scurit reposant sur limmutabilit de String, une initialisation non sre pourrait crer des failles permettant un code malicieux de contourner les tests de scurit. 1
Une initialisation sre garantit que les threads verront les valeurs correctes des champs final de tout objet correctement construit tels quils ont t initialiss par le constructeur et quelle que soit la faon dont a t publi lobjet. En outre, toute variable pouvant tre atteinte via un champ final dun objet correctement construit (comme les lments dun tableau final ou le contenu dun HashMap rfrenc par un champ final) est galement assure dtre visible par les autres threads 1.

Avec les objets ayant des champs final, linitialisation sre empche de rorganiser la construction avec un chargement initial dune rfrence cet objet. Toutes les critures dans les champs final effectues par le constructeur, ainsi que toutes les variables accessibles au moyen de ces champs, deviennent "ges" lorsque le constructeur se termine, et tout thread obtenant une rfrence cet objet est assur de voir une valeur qui est au moins aussi jour que la valeur ge. Les critures qui initialisent les variables accessibles via des champs final ne sont pas rorganises pour tre mlanges avec les oprations qui suivent le gel postconstruction. Une initialisation sre signie quun objet de la classe SafeStates prsente dans le Listing 16.8 pourra tre publi en toute scurit, mme via une initialisation paresseuse non sre ou en dissimulant une rfrence un SafeStates dans un champ statique public sans synchronisation, bien que cette classe nutilise pas de synchronisation et quelle repose sur un HashSet non thread-safe.
Listing 16.8 : Initialisation sre pour les objets non modifiables.
@ThreadSafe public class SafeStates { private final Map<String, String> states;

1. Ceci ne sapplique quaux objets qui ne sont accessibles que par des champs final de lobjet en construction.

Chapitre 16

Le modle mmoire de Java

359

public SafeStates() { states = new HashMap<String, String>(); states.put("alaska", "AK"); states.put("alabama", "AL"); ... states.put("wyoming", "WY"); } public String getAbbreviation (String s) { return states.get(s); } }

Cependant, il sufrait de quelques modications pour que SafeStates ne soit plus thread-safe. Si states nest pas final ou si une autre mthode que le constructeur modie son contenu, linitialisation nest plus sufsamment sre pour que lon puisse accder SafeStates en toute scurit sans synchronisation. Si cette classe a dautres champs non final, les autres threads peuvent quand mme voir des valeurs incorrectes pour ces champs. En outre, autoriser lobjet schapper au cours de la construction invalide la garantie de scurit offerte par linitialisation.
Linitialisation sre ne donne des garanties de visibilit que pour les valeurs accessibles partir des champs final au moment o le constructeur se termine. Pour les valeurs accessibles via des champs non final ou celles qui peuvent tre modies aprs la construction, la visibilit ne peut tre garantie quau moyen de la synchronisation.

Rsum
Le modle mmoire de Java prcise les conditions dans lesquelles les actions dun thread sur la mmoire seront visibles aux autres threads. Il faut notamment sassurer que les actions se droulent selon un ordre partiel appel arrive-avant, qui est prcis au niveau des diffrentes oprations mmoire et de synchronisation. En labsence dune synchronisation sufsante, il peut se passer des choses trs tranges lorsque les threads accdent aux donnes partages. Cependant, les rgles de haut niveau dcrites aux Chapitres 2 et 3, comme @GuardedBy et la publication correcte, permettent de garantir une scurit vis--vis des threads sans devoir faire appel aux dtails de bas niveau de arrive-avant.

Annexe
Annotations pour la concurrence
Nous avons utilis des annotations comme @GuardedBy et @ThreadSafe pour montrer comment il tait possible de prciser les promesses de thread safety et les politiques de synchronisation. Cette annexe documente ces annotations ; leur code source peut tre tlcharg partir du site web consacr cet ouvrage (dautres promesses de thread safety et des dtails dimplmentation supplmentaires devraient, bien sr, tre galement documents, mais ils ne sont pas capturs par cet ensemble minimal dannotations).

A.1

Annotations de classes

Nous utilisons trois annotations au niveau des classes pour dcrire leurs promesses de thread safety : @Immutable, @ThreadSafe et @NotThreadSafe. @Immutable signie, bien sr, que la classe nest pas modiable et implique @ThreadSafe. Lannotation @NotThread Safe est facultative si une classe nest pas annote comme tant thread-safe, on doit supposer quelle ne lest pas ; toutefois, @NotThreadSafe permet de lindiquer explicitement. Ces annotations sont relativement discrtes et bncient la fois aux utilisateurs et aux dveloppeurs. Les premiers peuvent ainsi savoir immdiatement si une classe est threadsafe et les seconds, sil faut prserver les garanties de thread safety. En outre, les outils danalyse statique du code peuvent vrier que le code est conforme au contrat indiqu par lannotation en vriant, par exemple, quune classe annote par @Immutable est bien non modiable.

A.2

Annotations de champs et de mthodes

Les annotations de classes font partie de la documentation publique dune classe. Dautres aspects de la stratgie dune classe vis--vis des threads sont entirement destins aux dveloppeurs et napparaissent donc pas dans sa documentation publique.

362

Annotations pour la concurrence

Annexe Annexe

Les classes qui utilisent des verrous devraient indiquer quelles sont les variables dtat qui sont protges et par quels verrous. Une source classique de non-respect de thread safety d un oubli est lorsquune classe thread-safe qui utilise correctement le verrouillage pour protger son tat est ensuite modie pour lui ajouter une nouvelle variable dtat qui nest pas correctement protge par un verrou ou de nouvelles mthodes qui nutilisent pas le verrouillage adquat pour protger les variables dtat existantes. En prcisant les variables protges et les verrous qui les protgent, on peut empcher ces deux types doublis.
@GuardedBy(verrou) prcise quon ne devrait accder un champ ou une mthode qu condition de dtenir le verrou indiqu. Le paramtre verrou identie le verrou qui doit avoir t pris avant daccder au champ ou la mthode annote. Les valeurs possibles de verrou sont :
m

@GuardedBy("this"), qui indique un verrouillage interne sur lobjet contenant (celui auquel appartient la mthode ou le champ). @GuardedBy("nomChamp"), qui prcise le verrou associ lobjet rfrenc par le champ indiqu ; il peut sagir dun verrou interne (pour les champs qui ne rfrencent pas un Lock) ou dun verrou explicite (pour les champs qui font rfrence un objet Lock). @GuardedBy("NomClasse.nomChamp"), identique @GuardedBy("nomChamp"), mais qui dsigne un objet verrou contenu dans un champ statique dune autre classe. @GuardedBy("nomMthode()"), qui prcise que lobjet verrou est renvoy par lappel la mthode indique. @GuardedBy("NomClasse.class"), qui dsigne lobjet classe littral de la classe indique.

Lutilisation de @GuardedBy pour identier chaque variable dtat qui a besoin dun verrouillage et pour prciser les verrous qui les protgent permet de faciliter la maintenance, la relecture du code et lutilisation doutils danalyses automatiques pour dceler les ventuelles erreurs de thread safety.

Bibliographie
Ken Arnold, James Gosling et David Holmes, The Java Programming Language, Fourth Edition, Addison-Wesley, 2005. David F. Bacon, Ravi B. Konuru, Chet Murthy et Mauricio J. Serrano, "Thin Locks: Featherweight Synchronization for Java", dans SIGPLAN Conference on Programming Language Design and Implementation, pages 258-268, 1998 (http://citeseer.ist.psu .edu/bacon98thin.html). Joshua Bloch, Effective Java Programming Language Guide, Addison-Wesley, 2001. Joshua Bloch et Neal Gafter, Java Puzzlers, Addison-Wesley, 2005. Hans Boehm, "Destructors, Finalizers, and Synchronization", dans POPL 03: Proceedings of the 30th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pages 262-272, ACM Press, 2003 (http://doi.acm.org/10.1145/604131 .604153). Hans Boehm, "Finalization, Threads, and the Java Memory Model", JavaOne presentation, 2005 (http://developers.sun.com/learning/javaoneonline/2005/coreplatform/TS3281.pdf). Joseph Bowbeer, "The Last Word in Swing Threads", 2005 (http://java.sun.com/products/ jfc/tsc/articles/threads/threads3.html). Cliff Click, "Performance Myths Exposed", JavaOne presentation, 2003. Cliff Click, "Performance Myths Revisited", JavaOne presentation, 2005 (http://developers.sun.com/learning/javaoneonline/2005/coreplatform/TS-3268.pdf). Martin Fowler, "Presentation Model", 2005 (http://www.martinfowler.com/eaaDev/ PresentationModel.html). Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides, Design Patterns, AddisonWesley, 1995. Martin Gardner, "The fantastic combinations of John Conways new solitaire game Life", Scientic American, octobre 1970. James Gosling, Bill Joy, Guy Steele et Gilad Bracha, The Java Language Specication, Third Edition, Addison-Wesley, 2005. Tim Harris et Keir Fraser, "Language Support for Lightweight Transactions", dans OOPSLA 03: Proceedings of the 18th Annual ACM SIGPLAN Conference on ObjectOriented Programming, Systems, Languages, and Applications, pages 388-402, ACM Press, 2003 (http://doi.acm.org/10.1145/949305.949340).

364

Bibliographie

Tim Harris, Simon Marlow, Simon Peyton-Jones et Maurice Herlihy, "Composable Memory Transactions", dans PPoPP 05: Proceedings of the Tenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, pages 48-60, ACM Press, 2005 (http://doi.acm.org/10.1145/1065944.1065952). Maurice Herlihy, "Wait-Free Synchronization", ACM Transactions on Programming Languages and Systems, 13(1):124-149, 1991 (http://doi.acm.org/10. 1145/114005 .102808). Maurice Herlihy et Nir Shavit, Multiprocessor Synchronization and Concurrent Data Structures, Morgan-Kaufman, 2006. C. A. R. Hoare, "Monitors: An Operating System Structuring Concept", Communications of the ACM, 17(10):549-557, 1974 (http://doi.acm.org/10.1145/355620.361161). David Hovemeyer et William Pugh, "Finding Bugs is Easy", SIGPLAN Notices, 39 (12) : 92-106, 2004 (http://doi.acm.org/10.1145/1052883.1052895). Ramnivas Laddad, AspectJ in Action, Manning, 2003. Doug Lea, Concurrent Programming in Java, Second Edition, Addison-Wesley, 2000. Doug Lea, "JSR-133 Cookbook for Compiler Writers" (http://gee.cs.oswego.edu/dl/ jmm/cookbook.html). J. D. C. Little, "A proof of the Queueing Formula L = _W"", Operations Research, 9 : 383-387, 1961. Jeremy Manson, William Pugh et Sarita V. Adve, "The Java Memory Model", dans POPL 05: Proceedings of the 32nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pages 378-391, ACM Press, 2005 (http://doi .acm.org/10.1145/1040305.1040336). George Marsaglia, "XorShift RNGs", Journal of Statistical Software, 8(13), 2003 (http://www.jstatsoft.org/v08/i14). Maged M. Michael et Michael L. Scott, "Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms", dans Symposium on Principles of Distributed Computing, pages 267-275, 1996 (http://citeseer.ist.psu.edu/michael96simple.html). Mark Moir et Nir Shavit, "Concurrent Data Structures", dans Handbook of Data Structures and Applications, Chapitre 47, CRC Press, 2004. William Pugh et Jeremy Manson, "Java Memory Model and Thread Specication", 2004 (http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf). M. Raynal, Algorithms for Mutual Exclusion, MIT Press, 1986. William N. Scherer, Doug Lea et Michael L. Scott, "Scalable Synchronous Queues", dans 11th ACM SIGPLAN Symposium on Principles and Practices of Parallel Programming (PPoPP), 2006. R. K. Treiber, "Systems Programming: Coping with Parallelism", Technical Report RJ 5118, IBM Almaden Research Center, avril 1986. Andrew Wellings, Concurrent and Real-Time Programming in Java, John Wiley & Sons, 2004.

Index
A AbortPolicy, politique de saturation 179 AbstractQueuedSynchronizer, classe 351 acquire(), mthode de AbstractQueuedSynchronizer 318 acquireShared(), mthode de AbstractQueuedSynchronizer 318 afterExecute(), mthode de ThreadPoolExecutor 166, 183 Analyse des chappements 236 Annotations 7 Annulable, activit 140 Annulation points dannulation 143 politique 141 tches 140 Antische sur la concurrence 113 ArrayBlockingQueue, classe 94, 269 ArrayDeque, classe 97 ArrayIndexOutOfBoundsException, exception 84 itrations sur un Vector 85 AsynchronousCloseException, exception 152 AtomicBoolean, classe 332 AtomicInteger, classe 331, 333 AtomicLong, classe 332 AtomicMarkableReference, classe 343 AtomicReference, classe 25, 332, 333 AtomicReferenceFieldUpdater, classe 342 AtomicStampedReference, classe 343 Atomique, opration 20 Cache viction 112 pollution 112 CallerRunsPolicy, politique de saturation 179 cancel(), mthode de Future 150, 153 CancellationException, exception 130 Classes internes, publication des objets 43 thread-safe 18 ClosedByInterruptException, exception 152 Cohrence des caches 346 squentielle 347 C Attente tournante 237 availableProcessors(), mthode de Runtime 174 await() mthode de Condition 313 mthode de CountDownLatch 100, 174 mthode de CyclicBarrier 105 AWT, toolkit graphique 12 B Barrire mmoire 235 beforeExecute(), mthode de ThreadPoolExecutors 183 BlockingQueue classe 354 garanties de publication 55 interface 92

366

Index

Collections thread-safe, garanties de publication 55 compareAndSet() mthode de AtomicInteger 331 mthode de AtomicReference 333, 338 compareAndSetState(), mthode de AbstractQueuedSynchronizer 317 ConcurrentHashMap, classe 109, 247 ConcurrentLinkedQueue classe 232, 339 garanties de publication 55 ConcurrentMap, classe, garanties de publication 55 ConcurrentModicationException, exception, itrations 86 ConcurrentSkipListMap, classe 248 Connement au thread 195 des objets classe ThreadLocal 46 types primitifs 47 variables locales 47 variables volatiles 46 Constructeurs itrations caches 88 publication des objets 44 CopyOnWriteArrayList, classe, garanties de publication 55 CopyOnWriteArraySet, classe, garanties de publication 55 countDown(), mthode de CountDownLatch 100 CountDownLatch, classe 100 CyclicBarrier, classe 105, 266 D Data Race, dnition 21 Date, classe, garanties de publication 57 DelayQueue, classe 128 Diagrammes dentrelacement 7

DiscardOldestPolicy, politique de saturation 179 DiscardPolicy, politique de saturation 179 done(), mthode de FutureTask 202 E lision de verrou 236 Encapsulation et thread safety 59 tat dun objet 16 paississement de verrou 236 equals(), mthode des collections, itrations caches 88 tat dun objet 15, 59 accs concurrents 15, 16 encapsulation 16, 29 espace dtat 60 objets sans tat 19 oprations dpendantes de ltat 61 protection par verrous internes 30 publication 73 viction dun cache 112 Exchanger, classe 107 ExecutionException, exception 102, 130 F FindBugs9, analyse statique du code 278 Frontires entre les tches 117 Fuite de threads 163 Future classe 201 interface 101 FutureTask, classe 109, 351 G Garanties de publication 55 des objets, exigences 57 Gnrateur de nombres pseudo-alatoires 333

Index

367

get(), mthode de Future 101, 109, 131, 133, 151, 322 getState(), mthode de AbstractQueuedSynchronizer 317 H hashCode() mthode de Object 212 mthode des collections, itrations caches 88 HashMap, classe 247 Hashtable, classe, garanties de publication 55 I identityHashCode(), mthode de System 212 Initialisateurs statiques, garanties de publication 56 interrupt(), mthode de Thread 98 InterruptedException, exception 98 Interruption dun thread 98 mcanisme coopratif 139 Invariant de classe 25 variables impliques 25 violation 25 Inversion des priorits 327 invokeAll(), mthode de ExecutorService 136 invokeLater(), mthode Swing 45 iostat, commande Unix 246 isCancelled(), mthode de Future 203 isEmpty(), mthode de Map 244 isHeldExclusively(), mthode de AbstractQueuedSynchronizer 318 Itrations caches mthode des collections equals() 88 mthode des collections toString() 87 exception ConcurrentModicationException 86

J java.util.concurrent.atomic, paquetage 24 JavaServer Pages (JSP) 11 join(), mthode de Thread 174 JSP (JavaServer Pages) 11 L LinkedBlockingDeque, classe 97 LinkedBlockingQueue, classe 94, 178, 232, 269 LinkedList, classe 232 Lire-modier-crire, oprations composes 20 Little, loi 238 Lock striping 89 lockInterruptibly(), mthode de Lock 152, 287 M Mmozation 107 Mettre-si-absent, opration compose 30 mpstat, commande Unix 245 Mutex, implment par un smaphore binaire 103 MVC (Modle-Vue-Contrleur) 194 N newCachedThreadPool() mthode de ThreadPoolExecutor 176 mthode fabrique 124 newCondition(), mthode de Lock 313 newFixedThreadPool() mthode de ThreadPoolExecutor 176 mthode fabrique 124 newScheduledThreadExecutor(), mthode de ThreadPoolExecutor 176 newScheduledThreadPool (), mthode fabrique 125, 127

368

Index

newSingleThreadExecutor (), mthode fabrique 124 newThread(), mthode de ThreadFactory 181 newUpdater(), mthode de AtomicReferenceFieldUpdater 342 Nombres sur 64 bits, safety magique 38 Notication conditionnelle 309 notify(), mthode de Object 304, 308 notifyAll(), mthode de Object 303, 308 O offer(), mthode de BlockingQueue 92 Oprations atomiques 20 composes Lire-modier-crire 20 Mettre-si-absent 30 Optimisation du code, prcautions 17 Option -server, Hotspot 276 P perfmon, outil Windows 235, 245 Point dannulation 143 Politique dexcution 117 dinterruption 145 de saturation 179 de synchronisation 60, 79 distribution 76 poll(), mthode de BlockingQueue 92 Pollution d'un cache 112 prestartAllCoreThreads(), mthode de ThreadPoolExecutor 176 Priorit des threads vs. priorit du systme d'exploitation 222 PriorityBlockingQueue, classe 94 privilegedThreadFactory(), mthode de Executors 183

Publication des objets conditions de publication correcte 55 problmes de visibilit 54 valeurs obsoltes 54 put(), mthode de BlockingQueue 92, 174 putIfAbsent(), mthode de ConcurrentMap 111 Q Queue, interface 232 R Race condition 8 ReadWriteLock, interface 293 ReentrantLock, classe 333 ReentrantReadWriteLock, classe 294 RejectedExecutionException, exception 179 release(), mthode de AbstractQueuedSynchronizer 318 releaseShared(), mthode de AbstractQueuedSynchronizer 318 Remote Method Invocation (RMI) 11 RMI (Remote Method Invocation) 11 RuntimeException, exception 163 S Safety magique, nombres sur 64 bits 38 Semaphore, classe 103, 180 Srialisation, sources 232 Service de terminaison 133 Servlets, framework 11 setRejectedExecutionHandler(), mthode de ThreadPoolExecutor 179 setState(), mthode de AbstractQueuedSynchronizer 317 shutdownNow(), mthode de ExecutorService 161 signal(), mthode de Condition 313

Index

369

signalAll(), mthode de Condition 313 Situation de comptition lire-modier-crire 22 tester-puis-agir 22 size(), mthode de Map 244 SocketException, exception 152 submit(), mthode de ExecutorService 150 Swing connement des objets 45 toolkit graphique 12 SwingUtilities, classe 196 SwingWorker, classe 204 Synchronisation, politiques 29, 79 synchronized blocs 26 visibilit mmoire 39 mot-cl 16 synchronizedList, classe, garanties de publication 55 synchronizedMap, classe, garanties de publication 55 synchronizedSet, classe, garanties de publication 55 SynchronousQueue, classe 94 T take(), mthode de BlockingQueue 92 Terminaison, service 133 terminated(), mthode de ThreadPoolExecutors 183 this, rfrence, publication des objets 44 ThreadFactory, interface 181 ThreadInfo, classe 280 ThreadLocal, classe 336 connement des objets 46 Thread-safe classe 17 programme 17 TimeoutException, exception 135

TimerTask, classe 11, 127 toString(), mthode des collections, itrations caches 87 Transition d'tat 60 TreeMap, classe 248 TreeModel, modle de donnes de Swing 199 tryAcquire(), mthode de AbstractQueuedSynchronizer 318 tryAcquireShared(), mthode de AbstractQueuedSynchronizer 318 tryLock(), mthode de Lock 285 tryRelease(), mthode de AbstractQueuedSynchronizer 318 tryReleaseShared(), mthode de AbstractQueuedSynchronizer 318 Types primitifs, connement des objets 47 U uncongurableExecutorService (), mthode de Executors 183 V Variables locales, connement des objets 47 volatiles Voir Volatiles, variables Vector, classe garanties de publication 55 synchronisation 29 Verrou ct client 77 externe 77 moniteur 66 priv 66 public 66 visibilit mmoire 39 Verrouillage partitionn 89 protocoles 29 Visibilit mmoire 35 donnes obsoltes 37 verrous 39

370

Index

Vivacit, panne 9 vmstat, commande Unix 235, 245 Volatiles, variables connement des objets 46 critres dutilisation 41 synchronisation 40

W wait(), mthode de Object 303

Y yield(), mthode de Thread 222, 265

Programmation concurrente en
La programmation concurrente permet lexcution de programmes en parallle. lheure o les processeurs multicurs sont devenus un standard, elle est dsormais incontournable, et concerne tous les dveloppeurs Java. Mais lcriture dun code qui exploite efcacement la puissance des nouveaux processeurs et supporte les environnements concurrents reprsente un d la fois en termes darchitecture, de programmation et de tests. Le dveloppement, le test et le dbogage dapplications multithreads savrent en effet trs ardus car, videmment, les problmes de concurrence se manifestent de faon imprvisible. Ils apparaissent gnralement au pire moment en production, sous une lourde charge de travail. Le but de ce livre est de rpondre ces ds en offrant des techniques, des patrons et des outils pour analyser les programmes et pour encapsuler la complexit des interactions concurrentes. Il fournit la fois les bases thoriques et les techniques concrtes pour construire des applications concurrentes ables et adaptes aux systmes actuels et futurs.

Java
TABLE DES MATIRES

Rfrence

Thread Safety Partage des objets Composition dobjets Briques de base Excution des tches Annulation et arrt Pools de threads Applications graphiques viter les problmes de vivacit Performances et adaptabilit Tests des programmes concurrents Verrous explicites Construction de synchronisateurs personnaliss Variables atomiques et synchronisation non bloquante Le modle mmoire de Java Annotations pour la concurrence

propos de lauteur : Brian Goetz, consultant informatique, a vingt ans dexprience dans lindustrie du logiciel et a crit plus de 75 articles sur le dveloppement en Java. Il sest entour des principaux membres du Java Community Process JSR 166 Expert Group pour la rdaction de cet ouvrage.

Programmation

Niveau : Intermdiaire / Avanc Conguration : Multiplate-forme

ISBN : 978-2-7440-4109-9

Pearson Education France 47 bis, rue des Vinaigriers 75010 Paris Tl. : 01 72 74 90 00 Fax : 01 42 05 22 17 www.pearson.fr

You might also like