Une introduction aux variables atomiques en Java

Une introduction aux variables atomiques en Java

1. introduction

En termes simples, l'état partagé crée très facilement des problèmes lorsque la simultanéité est impliquée. Si l'accès aux objets mutables partagés n'est pas géré correctement, les applications peuvent rapidement devenir sujettes à des erreurs d'accès simultané difficiles à détecter.

Dans cet article, nous reviendrons sur l'utilisation des verrous pour gérer les accès simultanés, explorerons certains des inconvénients associés aux verrous et, enfin, présenterons les variables atomiques comme alternative.

2. Serrures

Jetons un œil à la classe:

public class Counter {
    int counter;

    public void increment() {
        counter++;
    }
}

Dans le cas d'un environnement à thread unique, cela fonctionne parfaitement. Cependant, dès que nous autorisons plusieurs threads à écrire, nous commençons à obtenir des résultats incohérents.

Ceci est dû à la simple opération d'incrémentation (counter++), qui peut ressembler à une opération atomique, mais en fait est une combinaison de trois opérations: obtenir la valeur, incrémenter et réécrire la valeur mise à jour.

Si deux threads tentent d'obtenir et de mettre à jour la valeur en même temps, cela peut entraîner la perte de mises à jour.

L'un des moyens de gérer l'accès à un objet consiste à utiliser des verrous. Cela peut être réalisé en utilisant le mot clésynchronized dans la signature de la méthodeincrement. Le mot clésynchronized garantit qu'un seul thread peut entrer la méthode à la fois (pour en savoir plus sur le verrouillage et la synchronisation, reportez-vous à -Guide to Synchronized Keyword in Java):

public class SafeCounterWithLock {
    private volatile int counter;

    public synchronized void increment() {
        counter++;
    }
}

De plus, nous devons ajouter le mot-clévolatile pour garantir une bonne visibilité des références parmi les threads.

L'utilisation de verrous résout le problème. Cependant, les performances en prennent un coup.

Lorsque plusieurs threads tentent d'acquérir un verrou, l'un d'eux gagne, tandis que le reste des threads est bloqué ou suspendu.

The process of suspending and then resuming a thread is very expensive et affecte l'efficacité globale du système.

Dans un petit programme, tel que lecounter, le temps passé dans le changement de contexte peut devenir beaucoup plus que l'exécution réelle du code, réduisant ainsi considérablement l'efficacité globale.

3. Opérations atomiques

Il existe une branche de recherche axée sur la création d'algorithmes non bloquants pour des environnements concurrents. Ces algorithmes exploitent des instructions de machine atomique de bas niveau, telles que les procédures de comparaison et d'échange (CAS), afin de garantir l'intégrité des données.

Une opération CAS typique fonctionne sur trois opérandes:

  1. L'emplacement de mémoire sur lequel opérer (M)

  2. La valeur attendue existante (A) de la variable

  3. La nouvelle valeur (B) qui doit être définie

L'opération CAS met à jour atomiquement la valeur de M en B, mais uniquement si la valeur existante dans M correspond à A, sinon aucune action n'est entreprise.

Dans les deux cas, la valeur existante dans M est renvoyée. Cela combine trois étapes - obtenir la valeur, comparer la valeur et mettre à jour la valeur - en une seule opération au niveau de la machine.

Lorsque plusieurs threads tentent de mettre à jour la même valeur via CAS, l'un d'eux gagne et met à jour la valeur. However, unlike in the case of locks, no other thread gets suspended; au lieu de cela, ils sont simplement informés qu'ils n'ont pas réussi à mettre à jour la valeur. Les threads peuvent alors continuer à travailler et les changements de contexte sont complètement évités.

Une autre conséquence est que la logique de base du programme devient plus complexe. En effet, nous devons gérer le scénario lorsque l’opération CAS n’a pas réussi. Nous pouvons réessayer jusqu'à ce que tout réussisse, ou nous ne pouvons rien faire et continuer en fonction du cas d'utilisation.

4. Variables atomiques en Java

Les classes de variables atomiques les plus couramment utilisées en Java sontAtomicInteger,AtomicLong,AtomicBoolean etAtomicReference. Ces classes représentent respectivement unint,long,boolean et une référence d'objet qui peuvent être mis à jour de manière atomique. Les principales méthodes exposées par ces classes sont:

  • get() - récupère la valeur de la mémoire, de sorte que les modifications apportées par d'autres threads soient visibles; équivaut à lire une variablevolatile

  • set() - écrit la valeur dans la mémoire, de sorte que la modification soit visible par les autres threads; équivaut à écrire une variablevolatile

  • lazySet() - écrit éventuellement la valeur en mémoire, peut être réorganisé avec les opérations de mémoire pertinentes suivantes. Un cas d'utilisation consiste à annuler les références, par souci de garbage collection, auquel on ne pourra plus jamais accéder. Dans ce cas, de meilleures performances sont obtenues en retardant l'écriture de nullvolatile

  • compareAndSet() - identique à celui décrit dans la section 3, renvoie vrai quand il réussit, sinon faux

  • weakCompareAndSet() - identique à celui décrit dans la section 3, mais plus faible dans le sens où il ne crée pas d'ordres survenant avant. Cela signifie qu'il n'est pas forcément nécessaire de voir les mises à jour apportées aux autres variables

Un compteur thread-safe implémenté avecAtomicInteger est illustré dans l'exemple ci-dessous:

public class SafeCounterWithoutLock {
    private final AtomicInteger counter = new AtomicInteger(0);

    public int getValue() {
        return counter.get();
    }
    public void increment() {
        while(true) {
            int existingValue = getValue();
            int newValue = existingValue + 1;
            if(counter.compareAndSet(existingValue, newValue)) {
                return;
            }
        }
    }
}

Comme vous pouvez le voir, nous réessayons l'opérationcompareAndSet et à nouveau en cas d'échec, car nous voulons garantir que l'appel à la méthodeincrement augmente toujours la valeur de 1.

5. Conclusion

Dans ce didacticiel rapide, nous avons décrit un autre moyen de gérer la concurrence, qui permet d’éviter les inconvénients liés au verrouillage. Nous avons également examiné les principales méthodes exposées par les classes de variables atomiques en Java.

Comme toujours, les exemples sont tous disponiblesover on GitHub.

Pour explorer plus de classes qui utilisent en interne des algorithmes non bloquants, reportez-vous àa guide to ConcurrentMap.