Quand utilisons-nous AtomicReference?
Est-il nécessaire de créer des objets dans tous les programmes multithread?
Fournissez un exemple simple où AtomicReference devrait être utilisé.
La référence atomique doit être utilisée dans un contexte où vous devez effectuer des opérations simples atomic (c'est-à-dire thread safe, non trivial) sur une référence, pour laquelle la synchronisation sur moniteur est non approprié. Supposons que vous souhaitiez vérifier si un champ spécifique ne reste que si l'état de l'objet reste tel que vous l'avez coché pour la dernière fois:
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 la référence atomique, vous pouvez le faire même si l'objet cache
est partagé par plusieurs threads, sans utiliser synchronized
. En général, il est préférable d’utiliser des synchroniseurs ou le framework Java.util.concurrent
plutôt que de fonctionner à nu Atomic*
, à moins que vous ne sachiez ce que vous faites.
Deux excellentes références sur les arbres morts qui vous initieront à ce sujet:
Notez que (je ne sais pas si cela a toujours été vrai) référence assignation (c'est-à-dire =
) est elle-même atomique (mise à jour primitive types 64 bits comme long
ou double
peut 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 le Spécification de langage Java 3ed, Section 17.7 .
Une référence atomique est idéale à utiliser lorsque vous devez partager et changer l'état d'un objet immuable entre plusieurs threads. C'est une déclaration super dense, je vais donc la décomposer un peu.
Tout d'abord, un objet immuable est un objet qui n'est en réalité pas modifié après la construction. Les méthodes d'un objet immuable retournent fréquemment de nouvelles instances de la même classe. Certains exemples incluent les classes wrapper de Long et Double, ainsi que String, pour n'en nommer que quelques-uns. (Selon programmation simultanée sur la machine virtuelle) les objets immuables sont une partie essentielle de la simultanéité moderne).
Ensuite, pourquoi AtomicReference est préférable à un objet volatile 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 d'autres threads d'entrer entre-temps et de modifier la valeur au milieu de la nouvelle concaténation de chaînes. Ensuite, lorsque votre fil reprend, vous écrasez le travail de l'autre fil. Mais honnêtement, ce code fonctionnera, il aura l’air propre et rendra la plupart des gens heureux.
Léger problème. C'est lent. Surtout s'il y a beaucoup de discorde de cet objet verrou. C’est parce que la plupart des verrous requièrent un appel système du système d’exploitation, et votre thread bloquera le contexte et changera de contexte de la CPU pour laisser la place à d’autres processus.
L'autre option est d'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’avant. Mais il y a quelque chose de vraiment important qui se passe sous le capot dans AtomicRefrence, c'est comparer et échanger. C’est une instruction unique de la CPU, et non un appel du système d’exploitation, qui permet le basculement. C'est une instruction unique sur le CPU. Et comme il n'y a pas de verrou, il n'y a pas de changement de contexte dans le cas où le verrou est exercé, ce qui permet de gagner encore plus de temps!
L'attrape est, pour AtomicReferences, ceci n'utilise pas un appel .equals (), mais une comparaison == pour la valeur attendue. Assurez-vous donc que ce qui est attendu correspond à l'objet réel renvoyé par get dans la boucle.
Voici un cas d'utilisation pour AtomicReference:
Considérez cette classe qui agit comme une plage de nombres et utilise des variables individuelles AtmomicInteger pour maintenir les limites des nombres inférieurs et supérieurs.
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 check-then-act, mais elles n'utilisent pas un verrouillage suffisant pour les rendre atomiques. Si la plage de numéros est valide (0, 10) et qu'un thread appelle setLower (5), tandis qu'un autre thread appelle setUpper (4), avec un timing malheureux, les deux passeront les contrôles dans les ajusteurs et les deux modifications seront appliquées. Le résultat est que la plage contient maintenant (5, 4) un état invalide. Ainsi, alors que les AtomicIntegers sous-jacents sont sécurisés pour les threads, la classe composite ne l’est pas. Cela peut être corrigé en utilisant AtomicReference au lieu d'utiliser AtomicIntegers individuel pour les limites supérieure et inférieure.
public class CasNumberRange {
//Immutable
private static class IntPair {
final int lower; // Invariant: lower <= upper
final int upper;
...
}
private final AtomicReference<IntPair> values =
new AtomicReference<IntPair>(new IntPair(0, 0));
public int getLower() { return values.get().lower; }
public int getUpper() { return values.get().upper; }
public void setLower(int i) {
while (true) {
IntPair oldv = values.get();
if (i > oldv.upper)
throw new IllegalArgumentException(
"Can't set lower to " + i + " > upper");
IntPair newv = new IntPair(i, oldv.upper);
if (values.compareAndSet(oldv, newv))
return;
}
}
// similarly for setUpper
}
Vous pouvez utiliser AtomicReference lorsque vous appliquez des verrous optimistes. Vous avez un objet partagé et vous souhaitez le modifier à partir de plus d'un thread.
Comme d’autres threads l’ont peut-être modifié et/peut modifier entre ces 2 étapes. Vous devez le faire dans une opération atomique. c'est là qu'AtomicReference peut aider
Je ne parlerai pas beaucoup. Déjà, mes amis respectés ont apporté leur précieuse contribution. Le code courant complet à la fin de ce blog devrait éviter toute confusion. Il s'agit d'un petit programme de réservation de siège de film dans un scénario multithread.
Quelques faits élémentaires importants sont les suivants. 1> Différents threads ne peuvent contenir que des variables d'instance et des membres statiques dans l'espace de segment de mémoire. 2> Les lectures ou écritures volatiles sont complètement atomiques et sérialisées/se produisent 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 travaillant avec un volatile verra toujours la valeur la plus récente. AtomicReference utilise cette propriété de volatile.
Vous trouverez ci-dessous 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 indiqué ci-dessous.
private volatile V value;
get () renvoie simplement la dernière valeur de la variable (comme le font les volatiles de manière "se passe avant").
public final V get()
La méthode la plus importante d’AtomicReference est la suivante.
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 non sécurisée de Java. Cet appel de méthode unsafe appelle l'appel natif, qui appelle une seule instruction au processeur. "expect" et "update" chaque référence à un objet.
Si et seulement si la variable de membre d'instance AtomicReference "valeur" se réfère au même objet est désigné par "s'attendre", "update" est maintenant affecté à cette variable d'instance et "true" est renvoyé. Ou bien, false est renvoyé. Le tout est fait atomiquement. Aucun autre thread ne peut intercepter entre les deux. Comme il s’agit d’une opération à un seul processeur (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 ne vous aidera pas.
Je voudrais ajouter un code courant à part entière, qui peut être exécuté dans Eclipse. Cela effacerait beaucoup de confusion. Ici, 22 utilisateurs (discussions sur MyTh) essaient de réserver 20 places. Voici l'extrait de code suivi du code complet.
Extrait de code dans lequel 22 utilisateurs essaient de réserver 20 places.
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
}
}
}
}
Voici un cas d'utilisation très simple qui n'a rien à voir avec la sécurité des threads.
Pour partager un objet entre des invocations lambda, la AtomicReference
est 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 un bon design ou quoi que ce soit (c'est juste un exemple trivial), mais si vous avez le cas où vous devez partager un objet entre des invocations lambda, la AtomicReference
est une option.
En fait, vous pouvez utiliser n'importe quel objet contenant une référence, même une collection ne contenant qu'un seul élément. Cependant, AtomicReference convient parfaitement.
Quand utilisons-nous AtomicReference?
AtomicReference est un moyen flexible de mettre à jour la valeur de variable de manière atomique sans utiliser de synchronisation.
AtomicReference
prend en charge la programmation thread-free sans verrou sur des variables uniques.
Il existe de nombreuses façons d’atteindre la sécurité des threads avec une API de haut niveau simultanée . Les variables atomiques est l'une des multiples options.
Les objets Lock
prennent en charge les idiomes de verrouillage qui simplifient de nombreuses applications simultanées.
Executors
définit 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 permettent de réduire considérablement le besoin de synchronisation.
Les variables atomiques possèdent des fonctionnalités qui minimisent la synchronisation et permettent d'éviter les erreurs de cohérence de la mémoire.
Fournissez un exemple simple où AtomicReference devrait ê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é d'utiliser AtomicReference
dans tous les programmes multi-threadés.
Si vous souhaitez protéger une seule variable, utilisez AtomicReference
. Si vous souhaitez protéger un bloc de code, utilisez d'autres constructions telles que Lock
/synchronized
etc.