Compteur
, qui ne fait qu'incrémenter une valeur.
Exemple 68. Atomicité d'une opération : classe
SimpleCompteur
public class SimpleCompteur { private static int compte ; public void compte() { compte++ ; } public static int getCompte() { return compte ; } }
main()
.
Exemple 69. Atomicité d'une opération : utilisation de
SimpleCompteur
// dans une méthode main() // définition d'un thread de comptage Runnable compte = new Runnable() { public void run() { // chaque thread possède son propre compteur SimpleCompteur compteur = new SimpleCompteur() ; for (int i = 0 ; i < 100 ; i++) { compteur.compte() ; } } } ; // on lance 5 threads, qui comptent chacun jusqu'à 100 Thread [] compteurs = new Thread[5] ; // initialisation des threads for (int i = 0 ; i < compteurs.length ; i++) { compteurs[i] = new Thread(compte) ; } // lancement des threads for (int i = 0 ; i < compteurs.length ; i++) { compteurs[i].start() ; } for (int i = 0 ; i < compteurs.length ; i++) { // jette une InterruptedException compteurs[i].join() ; } // vérification de la valeur de compte System.out.println("Compteur = " + SimpleCompteur.getCompte()) ;
Compteur = 352La valeur 352 n'est pas stable : si l'on lance cette application, plusieurs fois, on verra qu'elle peut changer. De temps en temps, si l'on a de la chance, il se peut même qu'elle soit égale à 500 ! Pourtant c'est bien à cette valeur que l'on s'attend, qu'y a-t-il d'incorrect dans notre application ?
compte++
n'est pas atomique. Quand bien elle en a l'air, ce qui se passe réellement est la chose suivante :
compteur
;
compteur
;
compteur
.
T1
lit la valeur de
compteur
et la stocke. À cet instant précis il est interrompu par un
thread
T2
qui fait la même chose. Les deux
threads
T1
et
T2
ont donc à cet instant la même valeur numérique pour le champ
compteur
. Ils l'incrémentent tous les deux, et recopient cette valeur dans la classe
SimpleCompteur
(
compteur
est un champ statique). Notre champ
compteur
a donc subi deux opérations d'incrémentation, mais la seconde possédait une valeur obsolète. Cela donne l'impression qu'il n'a été incrémenté qu'une seule fois.
compteur
. Modifions donc notre classe
SimpleCompteur
.
Exemple 70. Classe
SimpleCompteur
synchronisée
public class SimpleCompteur {
private static int compte ;
// objet de synchronization
private static Object key = new Object() ;
public void compte() {
synchronized(key) {
compte++ ;
}
}
public static int getCompte() {
return compte ;
}
}
static
, alors la synchronisation n'aurait pas fonctionné. Pourquoi ?
Chaque
thread
lancé possède sa propre instance de la classe
SimpleCompteur
. L'objet qui garde un bloc synchronisé doit obligatoirement être le même pour tous les
threads
qui tentent d'exécuter ce bloc. Si l'on ne déclare pas ce champ
static
, il sera propre à chaque
thread
, et sa clé sera donc toujours disponible.
Cette version de la classe
SimpleCompteur
fonctionne, mais pose les problèmes que nous avons déjà évoqués : perte de performance du fait d'un goulet d'étranglement. Si le nombre de
threads
est très grand, ce problème peut devenir important.
AtomicInteger
.
Exemple 71. Classe
SimpleCompteur
utilisant un
AtomicInteger
public class SimpleCompteur {
private static AtomicInteger compte = new AtomicInteger(0) ;
public void compte() {
compte.incrementAndGet() ;
}
public static int getCompte() {
return compte.intValue() ;
}
}
incrementAndGet()
incrémente l'entier stocké dans l'instance d'
AtomicInteger
de façon atomique. Notre application fonctionne donc correctement.
Si l'on augmente le nombre de
threads
et d'incrémentations, on se rend compte que cette version de notre application exemple est environ deux fois plus rapide à s'exécuter que si l'on utilise un bloc synchronisé.
java.util.concurrent.atomic
, un certain nombre de classes dont certaines permettent de gérer des variables atomiques.
Dans leur version fournie avec le JDK, ces variables n'utilisent pas de synchronisation, ce qui leur permet d'être très performantes. Il faut noter qu'elles ne sont pas faites pour remplacer les classes enveloppe des types de base (
Integer
,
Float
, etc...). Notamment ces classes ne surchargent pas la méthode
equals()
et ne sont pas
Comparable
. On ne peut donc raisonnablement pas les utiliser comme clé dans des tables de hachage.
Voyons ces classes.
AtomicBoolean
,
AtomicInteger
,
AtomicLong
et
AtomicReference
: permettent de gérer les booléens, les entiers 32 bits et 64 bits, ainsi que les références sur d'autres objets.
AtomicIntegerArray
,
AtomicLongArray
et
AtomicReferenceArray
: permettent de gérer des tableaux d'entiers 32 et 64 bits, et les tableaux de références sur d'autres objets.
AtomicInteger
. Cette classe expose deux constructeurs : le premier ne prend pas d'argument, et encapsule un entier de valeur 0, le second prend un
int
en argument, dont la valeur sera celle encapsulée par cet
AtomicInteger
.
get()
,
intValue()
,
longValue()
,
floatValue()
,
doubleValue()
: permettent de retourner la valeur encapsulée sous différentes formes.
set(int newValue)
: remplace la valeur encapsulée courante par celle passée en paramètre.
incrementAndGet()
,
decrementAndGet()
et
addAndGet(int delta)
: ces trois méthodes retournent un
int
, dont la valeur est le résultat de l'incrémentation, de la décrémentation, et de l'ajout de
delta
à la valeur encapsulée. Ces opérations sont atomiques.
getAndIncrement()
,
getAndDecrement()
et
getAndAdd(int delta)
: ces trois méthodes retournent un
int
, dont la valeur est la valeur encapsulée, puis incrémente, décrémente ou ajoute la valeur
delta
à cette valeur. Ces opérations sont atomiques.
compareAndSet(int expect, int value)
: compare la valeur encapsulée avec
expect
, si ces deux valeurs sont égales, alors la valeur encapsulée devient
value
. Dans le cas d'objets, la comparaison utilise
==
et non pas la méthode
equals()
.