Quand utiliser AtomicReference en Java?

311

Quand utilisons-nous AtomicReference?

Est-il nécessaire de créer des objets dans tous les programmes multithread?

Fournissez un exemple simple où AtomicReference doit être utilisé.

Chintu
la source

Réponses:

215

La référence atomique doit être utilisée dans un paramètre où vous devez effectuer des opérations atomiques simples (c'est -à- dire thread-safe , non triviales) sur une référence, pour lesquelles la synchronisation basée sur moniteur n'est pas appropriée. Supposons que vous souhaitiez vérifier si un champ spécifique uniquement si l'état de l'objet reste le même que lors de votre dernière vérification:

AtomicReference<Object> cache = new AtomicReference<Object>();

Object cachedValue = new Object();
cache.set(cachedValue);

//... time passes ...
Object cachedValueToUpdate = cache.get();
//... do some work to transform cachedValueToUpdate into a new version
Object newValue = someFunctionOfOld(cachedValueToUpdate);
boolean success = cache.compareAndSet(cachedValue,cachedValueToUpdate);

En raison de la sémantique de référence atomique, vous pouvez le faire même si l' cacheobjet est partagé entre les threads, sans utiliser synchronized. En général, il vaut mieux utiliser des synchroniseurs ou le java.util.concurrentframework plutôt que bare Atomic*sauf si vous savez ce que vous faites.

Deux excellentes références d'arbre mort qui vous présenteront ce sujet:

Notez que (je ne sais pas si cela a toujours été vrai) l' affectation de référence (c'est-à-dire =) est elle-même atomique (la mise à jour des types primitifs 64 bits comme longou doublepeut ne pas être atomique; mais la mise à jour d'une référence est toujours atomique, même si elle est 64 bits ) sans utiliser explicitement un Atomic*.
Voir la spécification Java Language 3ed, Section 17.7 .

andersoj
la source
43
Corrigez-moi si je me trompe, mais il semble que la clé pour en avoir besoin soit parce que vous devez faire un "compareAndSet". Si tout ce que je devais faire était défini, je n'aurais pas du tout besoin de l'AtomicObject car les mises à jour de référence elles-mêmes sont atomiques?
sMoZely
Est-il sûr de faire cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate))? C'est à dire en ligne le calcul?
kaqqao
4
@veggen Les arguments de fonction en Java sont évalués avant la fonction elle-même, donc l'inlining ne fait aucune différence dans ce cas. Oui, c'est sûr.
Dmitry
29
@sMoZely C'est correct, mais si vous n'utilisez pas, AtomicReferencevous devez marquer la variable volatileparce que tandis que le runtime garantit que l'attribution de référence est atomique, le compilateur peut effectuer des optimisations en supposant que la variable n'a pas été modifiée par d'autres threads.
kbolino
1
@BradCupit noter que je l' ai dit « si vous ne l' aide AtomicReference»; si vous êtes l' utilisez, mon conseil serait d'aller dans la direction opposée et le marquer de finalsorte que le compilateur peut optimiser en conséquence.
kbolino
91

Une référence atomique est idéale à utiliser lorsque vous devez partager et modifier l'état d'un objet immuable entre plusieurs threads. C'est une déclaration super dense donc je vais la décomposer un peu.

Premièrement, un objet immuable est un objet qui n'est effectivement pas modifié après la construction. Souvent, les méthodes d'un objet immuable renvoient de nouvelles instances de cette même classe. Quelques exemples incluent les classes wrapper de Long et Double, ainsi que String, pour n'en nommer que quelques-unes. (Selon la programmation simultanée sur la JVM, les objets immuables sont un élément essentiel de la concurrence moderne).

Ensuite, pourquoi AtomicReference est meilleur qu'un objet volatil pour partager cette valeur partagée. Un exemple de code simple montrera la différence.

volatile String sharedValue;
static final Object lock=new Object();
void modifyString(){
  synchronized(lock){
    sharedValue=sharedValue+"something to add";
  }
}

Chaque fois que vous souhaitez modifier la chaîne référencée par ce champ volatile en fonction de sa valeur actuelle, vous devez d'abord obtenir un verrou sur cet objet. Cela empêche un autre thread d'entrer pendant ce temps et de modifier la valeur au milieu de la nouvelle concaténation de chaînes. Ensuite, lorsque votre thread reprend, vous encombrez le travail de l'autre thread. Mais honnêtement, ce code fonctionnera, il a l'air propre et il rendrait la plupart des gens heureux.

Léger problème. C'est lent. Surtout s'il y a beaucoup de conflits avec cet objet verrou. C'est parce que la plupart des verrous nécessitent un appel système OS, et votre thread se bloquera et sera contextualisé hors du CPU pour faire place à d'autres processus.

L'autre option consiste à utiliser un AtomicRefrence.

public static AtomicReference<String> shared = new AtomicReference<>();
String init="Inital Value";
shared.set(init);
//now we will modify that value
boolean success=false;
while(!success){
  String prevValue=shared.get();
  // do all the work you need to
  String newValue=shared.get()+"lets add something";
  // Compare and set
  success=shared.compareAndSet(prevValue,newValue);
}

Maintenant, pourquoi est-ce mieux? Honnêtement, ce code est un peu moins propre qu'auparavant. Mais il y a quelque chose de vraiment important qui se passe sous le capot dans AtomicRefrence, à savoir comparer et échanger. C'est une seule instruction CPU, et non un appel OS, qui fait que le changement se produit. Il s'agit d'une seule instruction sur le CPU. Et comme il n'y a pas de verrous, il n'y a pas de changement de contexte dans le cas où le verrou s'exerce ce qui fait gagner encore plus de temps!

La capture est, pour AtomicReferences, cela n'utilise pas un appel .equals (), mais plutôt une comparaison == pour la valeur attendue. Assurez-vous donc que l'attendu est l'objet réel renvoyé par get in the loop.

Erik Helleren
la source
14
Vos deux exemples se comportent différemment. Il faudrait boucler workedpour obtenir la même sémantique.
CurtainDog
5
Je pense que vous devriez initialiser la valeur à l'intérieur du constructeur AtomicReference, sinon un autre thread peut toujours voir la valeur null avant d'appeler shared.set. (À moins que shared.set ne soit exécuté dans un initialiseur statique.)
Henno Vermeulen
8
Dans votre deuxième exemple, vous devriez, à partir de Java 8, utiliser quelque chose comme: shared.updateAndGet ((x) -> (x + "permet d'ajouter quelque chose")); ... qui appellera à plusieurs reprises le .compareAndSet jusqu'à ce qu'il fonctionne. Cela équivaut au bloc synchronisé qui réussirait toujours. Vous devez vous assurer que le lambda que vous transmettez est sans effet secondaire, car il peut être appelé plusieurs fois.
Tom Dibble
2
Il n'est pas nécessaire de créer une valeur partagée de chaîne volatile. Le synchronisé (verrou) est assez bon pour établir l'événement avant la relation.
Jai Pandit
2
"... changer l'état d'un objet immuable" est imprécis ici, en partie parce qu'étant littéral, vous ne pouvez pas changer l'état d'un objet immuable. L'exemple montre comment changer la référence d'une instance d'objet immuable à une autre. Je me rends compte que c'est pédant mais je pense que cela vaut la peine d'être souligné étant donné à quel point la logique Thread peut être déroutante.
Mark Phillips
30

Voici un cas d'utilisation pour AtomicReference:

Considérez cette classe qui agit comme une plage de nombres et utilise des variables AtmomicInteger individuelles pour maintenir les bornes numériques inférieures et supérieures.

public class NumberRange {
    // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i) {
        // Warning -- unsafe check-then-act
        if (i > upper.get())
            throw new IllegalArgumentException(
                    "can't set lower to " + i + " > upper");
        lower.set(i);
    }

    public void setUpper(int i) {
        // Warning -- unsafe check-then-act
        if (i < lower.get())
            throw new IllegalArgumentException(
                    "can't set upper to " + i + " < lower");
        upper.set(i);
    }

    public boolean isInRange(int i) {
        return (i >= lower.get() && i <= upper.get());
    }
}

SetLower et setUpper sont des séquences de vérification puis d'action, mais elles n'utilisent pas un verrouillage suffisant pour les rendre atomiques. Si la plage de numéros est maintenue (0, 10) et qu'un thread appelle setLower (5) tandis qu'un autre thread appelle setUpper (4), avec un timing malchanceux, les deux passeront les vérifications dans les setters et les deux modifications seront appliquées. Le résultat est que la plage détient désormais (5, 4) un état non valide. Ainsi, alors que les AtomicIntegers sous-jacents sont thread-safe, la classe composite ne l'est pas. Cela peut être résolu en utilisant un AtomicReference au lieu d'utiliser des AtomicIntegers individuels pour les limites supérieures et inférieures.

public class CasNumberRange {
    // Immutable
    private static class IntPair {
        final int lower;  // Invariant: lower <= upper
        final int upper;

        private IntPair(int lower, int upper) {
            this.lower = lower;
            this.upper = upper;
        }
    }

    private final AtomicReference<IntPair> values = 
            new AtomicReference<IntPair>(new IntPair(0, 0));

    public int getLower() {
        return values.get().lower;
    }

    public void setLower(int lower) {
        while (true) {
            IntPair oldv = values.get();
            if (lower > oldv.upper)
                throw new IllegalArgumentException(
                    "Can't set lower to " + lower + " > upper");
            IntPair newv = new IntPair(lower, oldv.upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }

    public int getUpper() {
        return values.get().upper;
    }

    public void setUpper(int upper) {
        while (true) {
            IntPair oldv = values.get();
            if (upper < oldv.lower)
                throw new IllegalArgumentException(
                    "Can't set upper to " + upper + " < lower");
            IntPair newv = new IntPair(oldv.lower, upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }
}
Binita Bharati
la source
2
Cet article est similaire à votre réponse, mais approfondit des choses plus compliquées. C'est intéressant! ibm.com/developerworks/java/library/j-jtp04186
LppEdd
20

Vous pouvez utiliser AtomicReference lors de l'application de verrous optimistes. Vous avez un objet partagé et vous souhaitez le modifier à partir de plusieurs threads.

  1. Vous pouvez créer une copie de l'objet partagé
  2. Modifier l'objet partagé
  3. Vous devez vérifier que l'objet partagé est toujours le même qu'avant - si oui, mettez à jour avec la référence de la copie modifiée.

Comme un autre thread peut l'avoir modifié et / peut modifier entre ces 2 étapes. Vous devez le faire dans une opération atomique. c'est là qu'AtomicReference peut vous aider

HamoriZ
la source
7

Voici un cas d'utilisation très simple et n'a rien à voir avec la sécurité des threads.

Pour partager un objet entre des invocations lambda, AtomicReferenceest une option :

public void doSomethingUsingLambdas() {

    AtomicReference<YourObject> yourObjectRef = new AtomicReference<>();

    soSomethingThatTakesALambda(() -> {
        yourObjectRef.set(youObject);
    });

    soSomethingElseThatTakesALambda(() -> {
        YourObject yourObject = yourObjectRef.get();
    });
}

Je ne dis pas que c'est une bonne conception ou quoi que ce soit (c'est juste un exemple trivial), mais si vous avez le cas où vous devez partager un objet entre les invocations lambda, AtomicReferencec'est une option.

En fait, vous pouvez utiliser n'importe quel objet qui contient une référence, même une collection qui n'a qu'un seul élément. Cependant, l'AtomicReference est un ajustement parfait.

Benny Bottema
la source
6

Je ne parlerai pas beaucoup. Mes amis amis respectés ont déjà donné leur précieuse contribution. Le code de course complet à la fin de ce blog devrait éliminer toute confusion. Il s'agit d'un petit programme de réservation de place de cinéma dans un scénario multi-thread.

Certains faits élémentaires importants sont les suivants. 1> Différents threads ne peuvent s'affronter que par exemple et les variables de membre statique dans l'espace de tas. 2> La lecture ou l'écriture volatile est complètement atomique et sérialisée / se produit avant et uniquement à partir de la mémoire. En disant cela, je veux dire que toute lecture suivra l'écriture précédente en mémoire. Et toute écriture suivra la lecture précédente de la mémoire. Ainsi, tout thread fonctionnant avec un volatile verra toujours la valeur la plus à jour. AtomicReference utilise cette propriété de volatile.

Voici une partie du code source d'AtomicReference. AtomicReference fait référence à une référence d'objet. Cette référence est une variable membre volatile dans l'instance AtomicReference comme ci-dessous.

private volatile V value;

get () renvoie simplement la dernière valeur de la variable (comme le font les volatiles de manière "antérieure").

public final V get()

Voici la méthode la plus importante d'AtomicReference.

public final boolean  compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

La méthode compareAndSet (expect, update) appelle la méthode compareAndSwapObject () de la classe dangereuse de Java. Cet appel de méthode de unsafe appelle l'appel natif, qui appelle une seule instruction au processeur. "attendre" et "mettre à jour" référencent chacun un objet.

Si et seulement si la variable de membre d'instance AtomicReference "valeur" fait référence au même objet est référencée par "expect", "update" est assigné à cette variable d'instance maintenant, et "true" est retourné. Sinon, false est retourné. Le tout se fait atomiquement. Aucun autre thread ne peut intercepter entre les deux. Comme il s'agit d'une opération à processeur unique (magie de l'architecture informatique moderne), elle est souvent plus rapide que l'utilisation d'un bloc synchronisé. Mais rappelez-vous que lorsque plusieurs variables doivent être mises à jour de manière atomique, AtomicReference n'aidera pas.

Je voudrais ajouter un code de course à part entière, qui peut être exécuté en éclipse. Cela dissiperait beaucoup de confusion. Ici, 22 utilisateurs (fils MyTh) tentent de réserver 20 sièges. Voici l'extrait de code suivi du code complet.

Extrait de code dans lequel 22 utilisateurs tentent de réserver 20 sièges.

for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }

Voici le code complet en cours d'exécution.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class Solution {

    static List<AtomicReference<Integer>> seats;// Movie seats numbered as per
                                                // list index

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        seats = new ArrayList<>();
        for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }
        for (Thread t : ths) {
            t.join();
        }
        for (AtomicReference<Integer> seat : seats) {
            System.out.print(" " + seat.get());
        }
    }

    /**
     * id is the id of the user
     * 
     * @author sankbane
     *
     */
    static class MyTh extends Thread {// each thread is a user
        static AtomicInteger full = new AtomicInteger(0);
        List<AtomicReference<Integer>> l;//seats
        int id;//id of the users
        int seats;

        public MyTh(List<AtomicReference<Integer>> list, int userId) {
            l = list;
            this.id = userId;
            seats = list.size();
        }

        @Override
        public void run() {
            boolean reserved = false;
            try {
                while (!reserved && full.get() < seats) {
                    Thread.sleep(50);
                    int r = ThreadLocalRandom.current().nextInt(0, seats);// excludes
                                                                            // seats
                                                                            //
                    AtomicReference<Integer> el = l.get(r);
                    reserved = el.compareAndSet(null, id);// null means no user
                                                            // has reserved this
                                                            // seat
                    if (reserved)
                        full.getAndIncrement();
                }
                if (!reserved && full.get() == seats)
                    System.out.println("user " + id + " did not get a seat");
            } catch (InterruptedException ie) {
                // log it
            }
        }
    }

}    
sankar banerjee
la source
5

Quand utilisons-nous AtomicReference?

AtomicReference est un moyen flexible de mettre à jour la valeur de la variable atomiquement sans utiliser de synchronisation.

AtomicReference Prend en charge la programmation sans thread et sans verrouillage sur des variables uniques.

Il existe plusieurs façons d'assurer la sécurité des threads avec une API simultanée de haut niveau . Les variables atomiques sont l'une des multiples options.

Lock Les objets prennent en charge les idiomes de verrouillage qui simplifient de nombreuses applications simultanées.

Executorsdéfinir une API de haut niveau pour le lancement et la gestion des threads. Les implémentations d'exécuteur fournies par java.util.concurrent fournissent une gestion de pool de threads adaptée aux applications à grande échelle.

Les collections simultanées facilitent la gestion de grandes collections de données et peuvent réduire considérablement le besoin de synchronisation.

Les variables atomiques ont des fonctionnalités qui minimisent la synchronisation et aident à éviter les erreurs de cohérence de la mémoire.

Fournissez un exemple simple où AtomicReference doit être utilisé.

Exemple de code avec AtomicReference:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

Est-il nécessaire de créer des objets dans tous les programmes multithread?

Vous n'êtes pas obligé de l'utiliser AtomicReferencedans tous les programmes multithread.

Si vous souhaitez protéger une seule variable, utilisez AtomicReference. Si vous souhaitez protéger un bloc de code, utilisez d'autres constructions comme Lock/ synchronizedetc.

Ravindra babu
la source
-1

Un autre exemple simple consiste à effectuer une modification de thread sécurisé dans un objet de session.

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    return holder.get();
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    while (true) {
        HighScore old = holder.get();
        if (old.score >= newScore.score)
            break;
        else if (holder.compareAndSet(old, newScore))
            break;
    } 
}

Source: http://www.ibm.com/developerworks/library/j-jtp09238/index.html

Dherik
la source