Utilisations pratiques d'AtomicInteger


230

Je comprends en quelque sorte que AtomicInteger et d'autres variables atomiques permettent des accès simultanés. Dans quels cas cette classe est-elle généralement utilisée?

Réponses:


190

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).


1
Je pense que je comprends la première utilisation. Il s'agit de s'assurer que le compteur a été incrémenté avant qu'un attribut ne soit à nouveau accessible. Correct? Pourriez-vous donner un court exemple pour la deuxième utilisation?
James P.

8
Votre compréhension de la première utilisation est un peu vraie - elle garantit simplement que si un autre thread modifie le compteur entre les opérations readet 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.
Andrzej Doyle

3
"reste> 0? reste: reste + n;" dans cette expression, y a-t-il une raison pour ajouter le reste à n quand il est 0?
sandeepkunkunuru

101

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 AtomicIntegerspour 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 intset les synchronizeddéclarations appropriées , la beauté de la fonctionnalité AtomicIntegerest 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 intvaleur. 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.


2
Merci pour cette explication claire. Quels seraient les avantages d'utiliser un AtomicInteger sur une classe où toutes les méthodes sont synchronisées? Cette dernière serait-elle considérée comme "plus lourde"?
James P.

3
De mon point de vue, c'est principalement l'encapsulation que vous obtenez avec AtomicIntegers - la synchronisation se produit exactement sur ce dont vous avez besoin, et vous obtenez des méthodes descriptives qui se trouvent dans l'API publique pour expliquer le résultat souhaité. (De plus, dans une certaine mesure, vous avez raison, on finit souvent par synchroniser toutes les méthodes d'une classe qui est probablement trop grossière, bien qu'avec HotSpot effectuant des optimisations de verrouillage et les règles contre l'optimisation prématurée, je considère que la lisibilité est un plus que la performance.)
Andrzej Doyle

C'est une explication très claire et précise, merci !!
Akash5288

Enfin une explication qui m'a éclairé correctement.
Benny Bottema

58

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:
++iis i.incrementAndGet()
i++is i.getAndIncrement()
--iis i.decrementAndGet()
i--is i.getAndDecrement()
i = xis i.set(x)
x = iis isx = i.get()

Il existe également d'autres méthodes pratiques, comme compareAndSetouaddAndGet


37

L'utilisation principale de AtomicIntegerest 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 intsont déjà atomiques mais AtomicIntegers'accompagnent de nombreuses opérations qui ne sont pas atomiques int.

Les plus simples sont les getAndXXXor 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. compareAndSetest très utile pour implémenter des sémaphores, des verrous, des verrous, etc.

L'utilisation de AtomicIntegerest 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 Integerpar exemple dans Maps comme valeurs.


1
Je ne pense pas que je voudrais l'utiliser AtomicIntegercomme 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.
Andrzej Doyle

1
@Andrzej bien sûr, non pas comme une clé qui doit être immuable mais une valeur.
gabuzo

@gabuzo Une idée pourquoi l'entier atomique fonctionne bien sur synchronisé?
Supun Wijerathne

Les tests sont assez anciens maintenant (plus de 6 ans) il pourrait être intéressant de retester avec un JRE récent. Je ne suis pas allé assez loin dans AtomicInteger pour répondre, mais comme il s'agit d'une tâche très spécifique, il utilisera des techniques de synchronisation qui ne fonctionnent que dans ce cas spécifique. Gardez également à l'esprit que le test est monothreadé et qu'un test similaire dans un environnement lourdement chargé pourrait ne pas donner une victoire aussi claire pour AtomicInteger
gabuzo

Je crois que ses 3 ms et 5,5 ms
Sathesh

18

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.


Merci d'avoir partagé cet exemple pratique. Cela ressemble à quelque chose que je devrais utiliser car j'ai besoin d'avoir un identifiant unique pour chaque fichier que j'importe dans mon programme :)
James P.

7

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.


7

En Java 8, les classes atomiques ont été étendues avec deux fonctions intéressantes:

  • int getAndUpdate (IntUnaryOperator updateFunction)
  • int updateAndGet (IntUnaryOperator updateFunction)

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 .


5

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.


4

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


3

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.


Je vois. Est-ce dans les cas où un attribut ou une instance agit comme une sorte de variable globale dans une application. Ou y a-t-il d'autres cas auxquels vous pouvez penser?
James P.

1

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.


0

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.