Recherche…


Introduction

Les types atomiques Java sont de simples types mutables qui fournissent des opérations de base qui sont sûres et atomiques sans avoir recours au verrouillage. Ils sont destinés à être utilisés dans des cas où le verrouillage constituerait un goulot d’étranglement ou un risque de blocage ou de blocage.

Paramètres

Paramètre La description
ensemble Ensemble volatil du champ
obtenir Lecture volatile du champ
lazySet Ceci est un magasin commandé opération du champ
compareAndSet Si la valeur est la valeur d’expiration, elle est envoyée à la nouvelle valeur
getAndSet obtenir la valeur actuelle et mettre à jour

Remarques

Beaucoup sur essentiellement des combinaisons de lectures ou écritures volatiles et des opérations CAS . La meilleure façon de comprendre cela est d'examiner directement le code source. Par exemple , AtomicInteger , Unsafe.getAndSet

Création de types atomiques

Pour un code multithread simple, l'utilisation de la synchronisation est acceptable. Cependant, l'utilisation de la synchronisation a un impact sur la vie, et comme une base de code devient plus complexe, il est probable que vous vous retrouviez avec un blocage , une famine ou un blocage .

Dans le cas d'une concurrence plus complexe, l'utilisation de variables atomiques constitue souvent une meilleure alternative, car elle permet d'accéder à une variable individuelle de manière sécurisée sans utiliser de méthodes synchronisées ou de blocs de code.

Créer un type AtomicInteger :

AtomicInteger aInt = new AtomicInteger() // Create with default value 0

AtomicInteger aInt = new AtomicInteger(1) // Create with initial value 1

De même pour d'autres types d'instance.

AtomicIntegerArray aIntArray = new AtomicIntegerArray(10) // Create array of specific length
AtomicIntegerArray aIntArray = new AtomicIntegerArray(new int[] {1, 2, 3}) // Initialize array with another array

De même pour d'autres types atomiques.

Il y a une exception notable: il n'y a pas de type float et double . Celles-ci peuvent être simulées en utilisant Float.floatToIntBits(float) et Float.intBitsToFloat(int) pour float ainsi que Double.doubleToLongBits(double) et Double.longBitsToDouble(long) pour les doubles.

Si vous souhaitez utiliser sun.misc.Unsafe vous pouvez utiliser n'importe quelle variable primitive comme atomique en utilisant l'opération atomique dans sun.misc.Unsafe . Tous les types primitifs doivent être convertis ou codés en int ou en longueurs pour l’utiliser de cette manière. Pour plus d'informations, voir: sun.misc.Unsafe .

Motivation pour les types atomiques

Le moyen le plus simple d'implémenter des applications multithread consiste à utiliser les primitives de synchronisation et de verrouillage intégrées à Java; Par exemple, le mot clé synchronized . L'exemple suivant montre comment nous pourrions utiliser synchronized comptes synchronized pour accumuler des comptes.

public class Counters {
    private final int[] counters;

    public Counters(int nosCounters) {
        counters = new int[nosCounters];
    }

    /**
     * Increments the integer at the given index
     */
    public synchronized void count(int number) {
        if (number >= 0 && number < counters.length) {
            counters[number]++;
        }
    }

    /**
     * Obtains the current count of the number at the given index,
     * or if there is no number at that index, returns 0.
     */
    public synchronized int getCount(int number) {
        return (number >= 0 && number < counters.length) ? counters[number] : 0;
    }
}

Cette implémentation fonctionnera correctement. Toutefois, si un grand nombre de threads effectuent de nombreux appels simultanés sur le même objet Counters , la synchronisation risque d'être un goulot d'étranglement. Plus précisément:

  1. Chaque appel de méthode synchronized commencera par le thread en cours qui acquiert le verrou de l'instance Counters .
  2. Le thread tiendra le verrou pendant qu'il vérifie la valeur du number et met à jour le compteur.
  3. Enfin, il libère le verrou, autorisant l'accès à d'autres threads.

Si un thread essaie d'acquérir le verrou alors qu'un autre le maintient, le thread qui tente de le bloquer sera bloqué à l'étape 1 jusqu'à ce que le verrou soit libéré. Si plusieurs threads sont en attente, l'un d'entre eux l'obtiendra et les autres continueront à être bloqués.

Cela peut entraîner quelques problèmes:

  • S'il y a beaucoup de conflits pour le verrou (c'est-à-dire que beaucoup de threads essaient de l'acquérir), alors certains threads peuvent être bloqués pendant longtemps.

  • Lorsqu'un thread est bloqué dans l'attente du verrou, le système d'exploitation essaie généralement de passer l'exécution à un autre thread. Ce changement de contexte entraîne un impact relativement important sur les performances du processeur.

  • Lorsqu'il y a plusieurs threads bloqués sur le même verrou, il n'y a aucune garantie que l'un d'entre eux soit traité "équitablement" (c'est-à-dire que l'exécution de chaque thread est garantie). Cela peut conduire à la famine du fil .

Comment met-on en œuvre les types atomiques?

Commençons par réécrire l'exemple ci-dessus en utilisant les compteurs AtomicInteger :

public class Counters {
    private final AtomicInteger[] counters;

    public Counters(int nosCounters) {
        counters = new AtomicInteger[nosCounters];
        for (int i = 0; i < nosCounters; i++) {
            counters[i] = new AtomicInteger();
        }
    }

    /**
     * Increments the integer at the given index
     */
    public void count(int number) {
        if (number >= 0 && number < counters.length) {
            counters[number].incrementAndGet();
        }
    }

    /**
     * Obtains the current count of the object at the given index,
     * or if there is no number at that index, returns 0.
     */
    public int getCount(int number) {
        return (number >= 0 && number < counters.length) ? 
                counters[number].get() : 0;
    }
}

Nous avons remplacé le int[] par un AtomicInteger[] et l’initialisé avec une instance dans chaque élément. Nous avons également ajouté des appels à incrementAndGet() et get() à la place des opérations sur les valeurs int .

Mais le plus important est de pouvoir supprimer le mot-clé synchronized car le verrouillage n'est plus requis. Cela fonctionne parce que les opérations incrementAndGet() et get() sont atomiques et thread-safe . Dans ce contexte, cela signifie que:

  • Chaque compteur dans le tableau ne sera observable que dans l'état "avant" pour une opération (comme un "incrément") ou dans l'état "après".

  • En supposant que l'opération a lieu à l'instant T , aucun thread ne pourra voir l'état "avant" après l'heure T

De plus, bien que deux threads puissent réellement tenter de mettre à jour la même instance AtomicInteger en même temps, les implémentations des opérations garantissent qu'un seul incrément se produit à la fois sur l'instance donnée. Cela se fait sans verrouillage, ce qui entraîne souvent de meilleures performances.

Comment fonctionnent les types atomiques?

Les types atomiques s'appuient généralement sur des instructions matérielles spécialisées dans le jeu d'instructions de la machine cible. Par exemple, les jeux d'instructions basés sur Intel fournissent une instruction CAS ( Compare and Swap ) qui effectuera une séquence spécifique d'opérations de mémoire de manière atomique.

Ces instructions de bas niveau sont utilisées pour implémenter des opérations de niveau supérieur dans les API des classes AtomicXxx respectives. Par exemple, (encore une fois, en pseudocode C-like):

private volatile num;

int increment() {
  while (TRUE) {
    int old = num;
    int new = old + 1;
    if (old == compare_and_swap(&num, old, new)) {
      return new;
    }
  }
}

S'il n'y a pas de conflit sur AtomicXxxx , le test if réussira et la boucle se terminera immédiatement. S'il y a contention, alors le if échouera pour tous les threads sauf un, et ils "tourneront" dans la boucle pour un petit nombre de cycles de la boucle. En pratique, la rotation est de l'ordre de grandeur plus rapide (sauf à des niveaux de conflit irréalistes , où les performances synchronisées sont meilleures que celles des classes atomiques car lorsque l'opération CAS échoue, alors la tentative ne fera qu'ajouter à la suspension) un.

Incidemment, les instructions CAS sont généralement utilisées par la JVM pour mettre en œuvre un verrouillage non réglementé . Si la JVM peut voir qu'un verrou n'est pas actuellement verrouillé, il tentera d'utiliser un CAS pour acquérir le verrou. Si le CAS réussit, il n'est alors pas nécessaire de procéder à la planification de thread, au changement de contexte, etc. Pour plus d'informations sur les techniques utilisées, voir Verrouillage biaisé dans HotSpot .



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow