Je comprends en quelque sorte qu'AtomicInteger et d'autres variables atomiques autorisent des accès simultanés. Dans quels cas cette classe est-elle généralement utilisée?
AtomicInteger
a deux utilisations principales:
En tant que compteur atomique (incrementAndGet()
, etc.) pouvant être utilisé simultanément par plusieurs threads
En tant que primitive prenant en charge compare-and-swap instruction (compareAndSet()
) pour mettre en œuvre des algorithmes non bloquants.
Voici un exemple de générateur de nombres aléatoires non bloquant de Java Concurrency In Practice de Brian Göetz :
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;
}
}
}
...
}
Comme vous pouvez le constater, il fonctionne pratiquement de la même manière que incrementAndGet()
, mais effectue des calculs arbitraires (calculateNext()
) au lieu d’augmenter (et traite le résultat avant le renvoi).
L'exemple le plus simple que je puisse imaginer consiste à incrémenter une opération atomique.
Avec les ints standard:
private volatile int counter;
public int getNextUniqueIndex() {
return counter++; // Not atomic, multiple threads could get the same result
}
Avec AtomicInteger:
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Ce dernier est un moyen très simple d’effectuer des effets de mutations simples (notamment le comptage ou l’indexation unique), sans avoir à recourir à la synchronisation de tous les accès.
Une logique sans synchronisation plus complexe peut être utilisée en utilisant compareAndSet()
comme type de verrouillage optimiste - obtenir la valeur actuelle, calculer le résultat sur cette base, définir ce résultat iff value est toujours l'entrée utilisée pour effectuer le calcul, sinon recommencez - mais les exemples de comptage sont très utiles, et j'utiliserai souvent AtomicIntegers
pour compter et des générateurs uniques à l'échelle de la machine virtuelle s'il y a le moindre indice que de multiples threads sont impliqués, car ils sont si faciles à utiliser que j'envisagerais presque il optimisation prématurée d'utiliser plain ints
.
Bien que vous puissiez presque toujours obtenir les mêmes garanties de synchronisation avec les déclarations ints
et synchronized
appropriées, la beauté de AtomicInteger
réside dans le fait que la sécurité des threads est intégrée à l'objet lui-même, plutôt que de vous inquiéter des liens possibles, de chaque méthode qui arrive à accéder à la valeur int
. Il est beaucoup plus difficile de violer accidentellement la sécurité des threads lorsque vous appelez getAndIncrement()
que lorsque vous renvoyez i++
et que vous vous souvenez (ou non) d’acquérir préalablement le bon ensemble de moniteurs.
Si vous examinez les méthodes d’AtomicInteger, vous remarquerez qu’elles tendent à correspondre aux opérations courantes sur les ints. Par exemple:
static AtomicInteger i;
// Later, in a thread
int current = i.incrementAndGet();
est la version thread-safe de ceci:
static int i;
// Later, in a thread
int current = ++i;
Les méthodes correspondent à ceci:++i
est i.incrementAndGet()
i++
est i.getAndIncrement()
--i
est i.decrementAndGet()
i--
est i.getAndDecrement()
i = x
est i.set(x)
x = i
est x = i.get()
Il existe également d'autres méthodes pratiques, telles que compareAndSet
ou addAndGet
AtomicInteger
est principalement utilisé lorsque vous vous trouvez dans un contexte multithread et que vous devez effectuer des opérations thread-safe sur un entier sans utiliser synchronized
. L'attribution et l'extraction sur le type primitif int
sont déjà atomiques, mais AtomicInteger
est livré avec de nombreuses opérations qui ne sont pas atomiques sur int
.
Les plus simples sont les getAndXXX
ou xXXAndGet
. Par exemple, getAndIncrement()
est un équivalent atomique de i++
qui n'est pas atomique car il s'agit en réalité d'un raccourci pour trois opérations: récupération, addition et assignation. compareAndSet
est très utile pour implémenter des sémaphores, des verrous, des verrous, etc.
Utiliser AtomicInteger
est plus rapide et lisible que d’effectuer la même chose avec la synchronisation.
Un test simple:
public synchronized int incrementNotAtomic() {
return notAtomic++;
}
public void performTestNotAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
incrementNotAtomic();
}
System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}
public void performTestAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
atomic.getAndIncrement();
}
System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}
Sur mon PC avec Java 1.6, le test atomique s'exécute en 3 secondes et le test synchronisé en 5,5 secondes environ. Le problème ici est que l'opération à synchroniser (notAtomic++
) est vraiment courte. Le coût de la synchronisation est donc très important par rapport à l'opération.
Outre atomicity, AtomicInteger peut être utilisé comme version modifiable de Integer
, par exemple dans Map
s en tant que valeurs.
Par exemple, j'ai une bibliothèque qui génère des instances d'une classe. Chacune de ces instances doit avoir un ID entier unique, car ces instances représentent des commandes envoyées à un serveur, et chaque commande doit avoir un ID unique. Étant donné que plusieurs threads sont autorisés à envoyer des commandes simultanément, j'utilise un AtomicInteger pour générer ces identifiants. Une approche alternative consisterait à utiliser une sorte de verrou et un entier régulier, mais c'est à la fois plus lent et moins élégant.
Comme dit gabuzo, j'utilise parfois AtomicIntegers lorsque je veux passer un int par référence. C'est une classe intégrée qui a un code spécifique à l'architecture. Il est donc plus facile et probablement plus optimisé que n'importe quel MutableInteger que je pourrais rapidement coder. Cela dit, cela ressemble à un abus de classe.
En Java 8, les classes atomiques ont été étendues avec deux fonctions intéressantes:
Tous deux utilisent updateFunction pour effectuer la mise à jour de la valeur atomique. La différence est que le premier retourne l'ancienne valeur et le second renvoie la nouvelle valeur. UpdateFunction peut être implémenté pour effectuer des opérations plus complexes de "comparaison et définition" par rapport à la version standard. Par exemple, il peut vérifier que le compteur atomique ne descend pas en dessous de zéro. Normalement, une synchronisation est nécessaire. Le code est sans verrouillage:
public class Counter {
private final AtomicInteger number;
public Counter(int number) {
this.number = new AtomicInteger(number);
}
/** @return true if still can decrease */
public boolean dec() {
// updateAndGet(fn) executed atomically:
return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
}
}
Le code provient de Java Atomic Example .
J'utilise généralement AtomicInteger lorsque j'ai besoin d'attribuer des ID à des objets pouvant être accédés ou créés à partir de plusieurs threads. Je l'utilise généralement comme attribut statique de la classe à laquelle j'accède dans le constructeur des objets.
Vous pouvez implémenter des verrous non bloquants à l'aide de compareAndSwap (CAS) sur des entiers atomiques ou des longs. Le "Logiciel Tl2" logiciel de mémoire transactionnelle papier décrit ceci:
Nous associons un verrou en écriture spécial versionné à chaque transaction emplacement de la mémoire. Dans sa forme la plus simple, le verrou en écriture avec version est un spinlock de Word simple qui utilise une opération CAS pour acquérir le verrou et un magasin pour le libérer. Puisqu'il suffit d'un seul bit pour indiquer que le verrou est pris, nous utilisons le reste du verrou Word pour tenir un numéro de version.
Ce qu'il décrit est d'abord lu le nombre entier atomique. Divisez cela en un bit de verrouillage ignoré et le numéro de version. Essayez de l'écrire en tant que bit de verrouillage effacé avec le numéro de version actuel dans l'ensemble de bits de verrouillage et le numéro de version suivant. Boucle jusqu'à ce que vous réussissiez et que vous soyez le fil conducteur du verrou. Déverrouillez en définissant le numéro de version actuel avec le bit de verrouillage effacé. L'article décrit l'utilisation des numéros de version dans les verrous pour coordonner le fait que les threads ont un ensemble cohérent de lectures lorsqu'ils écrivent.
Cet article décrit le fait que les processeurs disposent d’un support matériel pour les opérations de comparaison et d’échange, ce qui en fait un outil très efficace. Il prétend aussi:
les compteurs non bloquants à base de CAS utilisant des variables atomiques ont une meilleure performances supérieures aux compteurs basés sur le verrouillage dans les conflits faibles à modérés
La clé est qu'ils permettent l'accès simultané et la modification en toute sécurité. Ils sont couramment utilisés comme compteurs dans un environnement multithread. Avant leur introduction, il devait s'agir d'une classe écrite par l'utilisateur qui encapsulait les différentes méthodes dans des blocs synchronisés.
J'ai utilisé AtomicInteger pour résoudre le problème du philosophe mangeur.
Dans ma solution, les instances AtomicInteger étaient utilisées pour représenter les fourchettes, il en faut deux par philosophe. Chaque philosophe est identifié comme un entier, compris entre 1 et 5. Quand un philosophe utilise une fourche, AtomicInteger conserve la valeur du philosophe, 1 à 5, sinon la fourchette n'est pas utilisée et la valeur de AtomicInteger est donc -1 .
AtomicInteger permet ensuite de vérifier si un fork est libre, valeur == - 1, et de le définir sur le propriétaire du fork si libre, en une seule opération atomique. Voir le code ci-dessous.
AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){
if (Hungry) {
//if fork is free (==-1) then grab it by denoting who took it
if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
//at least one fork was not succesfully grabbed, release both and try again later
fork0.compareAndSet(p, -1);
fork1.compareAndSet(p, -1);
try {
synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork
lock.wait();//try again later, goes back up the loop
}
} catch (InterruptedException e) {}
} else {
//sucessfully grabbed both forks
transition(fork_l_free_and_fork_r_free);
}
}
}
Etant donné que la méthode compareAndSet ne bloque pas, elle devrait augmenter le débit et davantage de travail. Comme vous le savez peut-être, le problème Dining Philosophers est utilisé lorsqu'un accès contrôlé aux ressources est nécessaire, par exemple des fourchettes, comme si un processus avait besoin de ressources pour continuer à travailler.