Réponses:
Il existe deux utilisations principales de AtomicInteger
:
En tant que compteur atomique ( incrementAndGet()
, etc.) pouvant être utilisé simultanément par de nombreux threads
En tant que primitive prenant en charge l' instruction compare-and-swap ( compareAndSet()
) pour implémenter des algorithmes non bloquants.
Voici un exemple de générateur de nombres aléatoires non bloquant de la concurrence Java en pratique 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 voir, cela fonctionne essentiellement de la même manière que incrementAndGet()
, mais effectue des calculs arbitraires ( calculateNext()
) au lieu d'incrémenter (et traite le résultat avant le retour).
read
et write that value + 1
, cela est détecté plutôt que d'écraser l'ancienne mise à jour (en évitant le problème de "perte de mise à jour"). Il s'agit en fait d'un cas spécial de compareAndSet
- si l'ancienne valeur était 2
, la classe appelle en fait compareAndSet(2, 3)
- donc si un autre thread a modifié la valeur entre-temps, la méthode d'incrémentation redémarre effectivement depuis le début.
L'exemple le plus simple absolu auquel je puisse penser est de faire de l'incrémentation une opération atomique.
Avec des pouces 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 si la valeur siff est toujours l'entrée utilisée pour effectuer le calcul, sinon recommencer - mais le les exemples de comptage sont très utiles, et je vais souvent utiliser AtomicIntegers
pour le comptage et les générateurs uniques à l'échelle de la machine virtuelle s'il y a un soupçon de plusieurs threads impliqués, car ils sont si faciles à travailler que je considérerais presque comme une optimisation prématurée d'utiliser plain ints
.
Bien que vous puissiez presque toujours obtenir les mêmes garanties de synchronisation ints
et les synchronized
déclarations appropriées , la beauté de la fonctionnalité AtomicInteger
est que la sécurité des threads est intégrée à l'objet lui-même, plutôt que de vous soucier des entrelacements possibles et des moniteurs détenus de chaque méthode. cela arrive pour accéder à la int
valeur. Il est beaucoup plus difficile de violer accidentellement la sécurité des threads lors de l'appel getAndIncrement()
que lors du retour i++
et de la mémorisation (ou non) pour acquérir au préalable le bon ensemble de moniteurs.
Si vous regardez les méthodes d'AtomicInteger, vous remarquerez qu'elles ont tendance à correspondre aux opérations courantes sur les entiers. 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;
La carte des méthodes comme ceci:
++i
is i.incrementAndGet()
i++
is i.getAndIncrement()
--i
is i.decrementAndGet()
i--
is i.getAndDecrement()
i = x
is i.set(x)
x = i
is isx = i.get()
Il existe également d'autres méthodes pratiques, comme compareAndSet
ouaddAndGet
L'utilisation principale de AtomicInteger
est lorsque vous êtes dans un contexte multithread et que vous devez effectuer des opérations thread-safe sur un entier sans utiliser synchronized
. L'affectation et la récupération sur le type primitif int
sont déjà atomiques mais AtomicInteger
s'accompagnent de nombreuses opérations qui ne sont pas atomiques int
.
Les plus simples sont les getAndXXX
or xXXAndGet
. Par exemple, getAndIncrement()
est un équivalent atomique i++
qui n'est pas atomique car il s'agit en fait d'un raccourci pour trois opérations: récupération, addition et affectation. compareAndSet
est très utile pour implémenter des sémaphores, des verrous, des verrous, etc.
L'utilisation de AtomicInteger
est plus rapide et plus lisible que l'exécution de la même chose en utilisant 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 tandis que celui synchronisé s'exécute en 5,5 secondes environ. Le problème ici est que l'opération de synchronisation ( notAtomic++
) est vraiment courte. Le coût de la synchronisation est donc très important par rapport à l'opération.
A côté de l'atomicité, AtomicInteger peut être utilisé comme une version mutable de Integer
par exemple dans Map
s comme valeurs.
AtomicInteger
comme clé de carte, car elle utilise l' equals()
implémentation par défaut , ce qui n'est certainement pas ce que vous attendez de la sémantique si elle est utilisée dans une carte.
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 ID. Une approche alternative serait d'utiliser une sorte de verrou et un entier régulier, mais c'est à la fois plus lent et moins élégant.
Comme l'a dit gabuzo, parfois j'utilise AtomicIntegers quand je veux passer un int par référence. C'est une classe intégrée qui a du code spécifique à l'architecture, c'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 la classe.
En Java 8, les classes atomiques ont été étendues avec deux fonctions intéressantes:
Les deux utilisent la fonction updateFunction pour effectuer la mise à jour de la valeur atomique. La différence est que la première renvoie l'ancienne valeur et la seconde renvoie la nouvelle valeur. La fonction updateFunction peut être implémentée pour effectuer des opérations de «comparaison et de définition» plus complexes que les opérations standard. Par exemple, il peut vérifier que le compteur atomique ne descend pas en dessous de zéro, il nécessiterait normalement une synchronisation, et ici le code est sans verrou:
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 est tiré de Java Atomic Example .
J'utilise généralement AtomicInteger lorsque j'ai besoin de donner des ID à des objets qui peuvent être accédés ou créés à partir de plusieurs threads, et je l'utilise généralement comme attribut statique sur 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 papier "Tl2" Software Transactional Memory décrit ceci:
Nous associons un verrou d'écriture versionné spécial à chaque emplacement mémoire traité. Dans sa forme la plus simple, le verrou d'écriture versionné est un verrou tournant à mot unique qui utilise une opération CAS pour acquérir le verrou et un magasin pour le libérer. Puisqu'on n'a besoin que d'un seul bit pour indiquer que le verrou est pris, nous utilisons le reste du mot de verrouillage pour contenir un numéro de version.
Ce qu'il décrit, c'est d'abord lire l'entier atomique. Divisez ceci en un bit de verrouillage ignoré et le numéro de version. Tentative d'écrire CAS 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 thread qui possède le verrou. Déverrouillez en définissant le numéro de version actuel avec le bit de verrouillage effacé. Le document décrit l'utilisation des numéros de version dans les verrous pour coordonner que les threads ont un ensemble cohérent de lectures lorsqu'ils écrivent.
Cet article décrit que les processeurs prennent en charge le matériel pour les opérations de comparaison et d'échange, ce qui les rend très efficaces. Il affirme également:
les compteurs basés sur CAS non bloquants utilisant des variables atomiques ont de meilleures performances que les compteurs basés sur verrouillage dans des conflits faibles à modérés
La clé est qu'ils permettent un accès simultané et une modification en toute sécurité. Ils sont couramment utilisés comme compteurs dans un environnement multithread - avant leur introduction, cela devait être une classe écrite par l'utilisateur qui regroupait les différentes méthodes dans des blocs synchronisés.
J'ai utilisé AtomicInteger pour résoudre le problème du Dining Philosopher.
Dans ma solution, des instances AtomicInteger ont été utilisées pour représenter les fourches, il y en a deux par philosophe. Chaque philosophe est identifié comme un entier, de 1 à 5. Lorsqu'une fourchette est utilisée par un philosophe, l'AtomicInteger détient la valeur du philosophe, 1 à 5, sinon la fourchette n'est pas utilisée, la valeur de l'AtomicInteger est donc -1 .
L'AtomicInteger permet ensuite de vérifier si une fourchette est libre, valeur == - 1, et de la définir sur le propriétaire de la fourche si elle est 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);
}
}
}
Étant donné que la méthode compareAndSet ne bloque pas, elle devrait augmenter le débit, plus de travail est effectué. Comme vous le savez peut-être, le problème des philosophes de la restauration est utilisé lorsqu'un accès contrôlé aux ressources est nécessaire, c'est-à-dire des fourches, comme un processus a besoin de ressources pour continuer à travailler.
Exemple simple pour la fonction compareAndSet ():
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val = new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(0, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
L'imprimé est: valeur précédente: 0 La valeur a été mise à jour et c'est 6 Un autre exemple simple:
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val
= new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(10, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
L'imprimé est: Valeur précédente: 0 La valeur n'a pas été mise à jour