Quando utilizzare AtomicReference in Java?


311

Quando usiamo AtomicReference?

È necessario creare oggetti in tutti i programmi multithread?

Fornisci un semplice esempio di utilizzo di AtomicReference.

Risposte:


215

Il riferimento atomico deve essere utilizzato in un'impostazione in cui è necessario eseguire semplici operazioni atomiche (ovvero thread-safe , non banali) su un riferimento, per le quali la sincronizzazione basata su monitor non è appropriata. Supponiamo di voler verificare se un campo specifico solo se lo stato dell'oggetto rimane l'ultimo controllo:

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

A causa della semantica di riferimento atomico, puoi farlo anche se l' cacheoggetto è condiviso tra i thread, senza usare synchronized. In generale, è meglio usare i sincronizzatori o il java.util.concurrentframework piuttosto che nudi a Atomic*meno che non si sappia cosa si sta facendo.

Due eccellenti riferimenti ad albero morto che ti introdurranno a questo argomento:

Si noti che (non so se sia sempre stato vero) l' assegnazione di riferimento (ovvero =) è essa stessa atomica (l'aggiornamento di tipi primitivi a 64 bit come longo doublepotrebbe non essere atomico; ma l'aggiornamento di un riferimento è sempre atomico, anche se è a 64 bit ) senza usare esplicitamente un Atomic*.
Vedere la specifica del linguaggio Java 3ed, sezione 17.7 .


43
Correggimi se sbaglio, ma sembra che la chiave per averne bisogno sia perché devi fare un "compareAndSet". Se tutto ciò che dovevo fare fosse impostato, non avrei bisogno di AtomicObject perché gli aggiornamenti di riferimento stessi erano atomici?
circa

È sicuro fare cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate))? Vale a dire il calcolo?
Kaqqao,

4
@veggen Gli argomenti della funzione in Java vengono valutati prima della funzione stessa, quindi inline in questo caso non fa differenza. Sì, è sicuro.
Dmitry,

29
@sMoZely È corretto, ma se non si utilizza AtomicReferenceè necessario contrassegnare la variabile volatileperché, mentre il runtime garantisce che l'assegnazione di riferimento è atomica, il compilatore può eseguire ottimizzazioni supponendo che la variabile non sia stata modificata da altri thread.
Kbolino,

1
@BradCupit Nota che ho detto "se stai non usando AtomicReference"; se lo stai usando, il mio consiglio sarebbe di andare nella direzione opposta e contrassegnarlo in finalmodo che il compilatore possa ottimizzare di conseguenza.
Kbolino,

91

Un riferimento atomico è ideale da utilizzare quando è necessario condividere e modificare lo stato di un oggetto immutabile tra più thread. Questa è un'affermazione super densa, quindi la analizzerò un po '.

Innanzitutto, un oggetto immutabile è un oggetto che non viene effettivamente modificato dopo la costruzione. Spesso i metodi di un oggetto immutabile restituiscono nuove istanze di quella stessa classe. Alcuni esempi includono le classi wrapper di Long e Double, oltre a String, solo per citarne alcuni. (Secondo la programmazione della concorrenza sugli oggetti immutabili JVM sono una parte critica della concorrenza moderna).

Successivamente, perché AtomicReference è meglio di un oggetto volatile per condividere quel valore condiviso. Un semplice esempio di codice mostrerà la differenza.

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

Ogni volta che vuoi modificare la stringa a cui fa riferimento quel campo volatile in base al suo valore corrente, devi prima ottenere un blocco su quell'oggetto. Questo impedisce ad altri thread di entrare nel frattempo e modificare il valore nel mezzo della nuova concatenazione di stringhe. Quindi quando il tuo thread riprende, intaserai il lavoro dell'altro thread. Ma onestamente quel codice funzionerà, sembra pulito e renderebbe felici molte persone.

Leggero problema. È lento. Soprattutto se c'è molta contesa di quell'oggetto Lock. Questo perché la maggior parte dei blocchi richiede una chiamata di sistema del sistema operativo e il thread si bloccherà e verrà contestualizzato dalla CPU per far posto ad altri processi.

L'altra opzione è utilizzare 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);
}

Ora, perché è meglio? Sinceramente quel codice è un po 'meno pulito di prima. Ma c'è qualcosa di veramente importante che accade sotto il cofano in AtomicRefrence, ed è il confronto e lo scambio. È una singola istruzione della CPU, non una chiamata del sistema operativo, che consente lo switch. Questa è una singola istruzione sulla CPU. E poiché non ci sono blocchi, non esiste alcun interruttore di contesto nel caso in cui il blocco venga esercitato, il che consente di risparmiare ancora più tempo!

Il problema è, per AtomicReferences, che non utilizza una chiamata .equals (), ma invece un confronto == per il valore atteso. Quindi assicurati che l'atteso sia l'oggetto reale restituito da get in the loop.


14
I tuoi due esempi si comportano diversamente. Dovresti fare un giro workedper ottenere la stessa semantica.
CurtainDog,

5
Penso che dovresti inizializzare il valore all'interno del costruttore AtomicReference, altrimenti un altro thread potrebbe ancora vedere il valore null prima di chiamare shared.set. (A meno che shared.set non venga eseguito in un inizializzatore statico.)
Henno Vermeulen,

8
Nel tuo secondo esempio, a partire da Java 8 dovresti usare qualcosa come: shared.updateAndGet ((x) -> (x + "consente di aggiungere qualcosa")); ... che chiamerà ripetutamente .compareAndSet fino a quando non funziona. Ciò equivale al blocco sincronizzato che avrebbe sempre successo. Devi assicurarti che il lambda che passi sia privo di effetti collaterali, perché può essere chiamato più volte.
Tom Dibble,

2
Non è necessario rendere StringVVue String volatile. Il sincronizzato (blocco) è abbastanza buono da stabilire l'accaduto prima della relazione.
Jai Pandit,

2
"... cambia lo stato di un oggetto immutabile" qui non è preciso, in parte perché essendo letterale non puoi cambiare lo stato di un oggetto immutabile. L'esempio mostra la modifica del riferimento da un'istanza di oggetto immutabile a diversa. Mi rendo conto che è pedante, ma penso che valga la pena sottolineare, dato quanto può essere confusa la logica di Thread.
Mark Phillips,

30

Ecco un caso d'uso per AtomicReference:

Considera questa classe che funge da intervallo di numeri e utilizza le singole variabili AtmomicInteger per mantenere i limiti di numero inferiore e superiore.

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());
    }
}

Sia setLower che setUpper sono sequenze check-then-act, ma non usano un blocco sufficiente per renderle atomiche. Se l'intervallo di numeri è valido (0, 10) e un thread chiama setLower (5) mentre un altro thread chiama setUpper (4), con un certo sfortunato tempo entrambi passeranno i controlli nei setter e verranno applicate entrambe le modifiche. Il risultato è che l'intervallo ora mantiene (5, 4) uno stato non valido. Quindi, mentre gli AtomicIntegers sottostanti sono thread-safe, la classe composita non lo è. Questo può essere risolto utilizzando AtomicReference invece di utilizzare singoli AtomicInteger per i limiti superiore e inferiore.

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;
        }
    }
}

2
Questo articolo è simile alla tua risposta, ma approfondisce le cose più complicate. È interessante! ibm.com/developerworks/java/library/j-jtp04186
LppEdd

20

È possibile utilizzare AtomicReference quando si applicano blocchi ottimistici. Hai un oggetto condiviso e vuoi cambiarlo da più di 1 thread.

  1. È possibile creare una copia dell'oggetto condiviso
  2. Modifica l'oggetto condiviso
  3. È necessario verificare che l'oggetto condiviso sia sempre lo stesso di prima - in caso affermativo, aggiornare con il riferimento della copia modificata.

Poiché altri thread potrebbero averlo modificato e / possono modificare tra questi 2 passaggi. Devi farlo in un'operazione atomica. questo è dove AtomicReference può aiutare


7

Ecco un caso d'uso molto semplice e non ha nulla a che fare con la sicurezza del thread.

Per condividere un oggetto tra invocazioni lambda, AtomicReferenceè un'opzione :

public void doSomethingUsingLambdas() {

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

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

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

Non sto dicendo che questo sia un buon design o altro (è solo un esempio banale), ma se hai il caso in cui devi condividere un oggetto tra invocazioni lambda, AtomicReferenceè un'opzione.

In effetti puoi usare qualsiasi oggetto che contiene un riferimento, anche una Collezione che ha un solo oggetto. Tuttavia, AtomicReference si adatta perfettamente.


6

Non parlerò molto. Già i miei amici rispettati hanno dato il loro prezioso contributo. Il codice in esecuzione completo all'ultimo di questo blog dovrebbe eliminare qualsiasi confusione. Si tratta di un posto al cinema che prenota un piccolo programma in uno scenario multi-thread.

Alcuni fatti elementari importanti sono i seguenti. 1> Thread diversi possono contendere solo per esempio e variabili membro statiche nello spazio heap. 2> Lettura o scrittura volatile sono completamente atomiche e serializzate / avvengono prima e vengono eseguite solo dalla memoria. Dicendo questo intendo che qualsiasi lettura seguirà la precedente scrittura in memoria. E qualsiasi scrittura seguirà la lettura precedente dalla memoria. Quindi ogni thread che lavora con un volatile vedrà sempre il valore più aggiornato. AtomicReference utilizza questa proprietà di volatile.

Di seguito sono riportati alcuni dei codici sorgente di AtomicReference. AtomicReference si riferisce a un riferimento a un oggetto. Questo riferimento è una variabile membro volatile nell'istanza AtomicReference come di seguito.

private volatile V value;

get () restituisce semplicemente l'ultimo valore della variabile (come fanno i volatili in un modo "succede prima").

public final V get()

Di seguito è riportato il metodo più importante di AtomicReference.

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

Il metodo compareAndSet (prevede, aggiorna) chiama il metodo compareAndSwapObject () della classe non sicura di Java. Questa chiamata di metodo non sicura richiama la chiamata nativa, che richiama una singola istruzione al processore. "prevedono" e "aggiorna" ogni riferimento a un oggetto.

Se e solo se la variabile "value" del membro dell'istanza AtomicReference fa riferimento allo stesso oggetto viene definita da "prevedono", "update" viene assegnato a questa variabile di istanza ora e viene restituito "true". Altrimenti, viene restituito falso. Il tutto è fatto atomicamente. Nessun altro thread può intercettare in mezzo. Poiché si tratta di un'operazione a singolo processore (magia della moderna architettura informatica), spesso è più veloce dell'uso di un blocco sincronizzato. Ma ricorda che quando più variabili devono essere aggiornate atomicamente, AtomicReference non aiuta.

Vorrei aggiungere un codice di esecuzione completo, che può essere eseguito in eclissi. Cancellerebbe molta confusione. Qui 22 utenti (thread MyTh) stanno provando a prenotare 20 posti. Di seguito è riportato lo snippet di codice seguito dal codice completo.

Snippet di codice in cui 22 utenti stanno cercando di prenotare 20 posti.

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();
        }

Di seguito è riportato il codice completo in esecuzione.

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
            }
        }
    }

}    

5

Quando utilizziamo AtomicReference?

AtomicReference è un modo flessibile per aggiornare atomicamente il valore della variabile senza usare la sincronizzazione.

AtomicReference supporta la programmazione thread-safe senza blocco su singole variabili.

Esistono diversi modi per ottenere la sicurezza dei thread con API simultanee di alto livello . Le variabili atomiche sono una delle molteplici opzioni.

Lock gli oggetti supportano espressioni di blocco che semplificano molte applicazioni simultanee.

Executorsdefinire un'API di alto livello per l'avvio e la gestione dei thread. Le implementazioni di Executor fornite da java.util.concurrent forniscono la gestione del pool di thread adatta per applicazioni su larga scala.

Le raccolte simultanee semplificano la gestione di grandi raccolte di dati e possono ridurre notevolmente la necessità di sincronizzazione.

Le variabili atomiche hanno funzionalità che riducono al minimo la sincronizzazione e aiutano a evitare errori di coerenza della memoria.

Fornisci un semplice esempio di utilizzo di AtomicReference.

Codice di esempio con 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);

È necessario creare oggetti in tutti i programmi multithread?

Non è necessario utilizzare AtomicReferencein tutti i programmi multi thread.

Se si desidera proteggere una singola variabile, utilizzare AtomicReference. Se vuoi proteggere un blocco di codice, usa altri costrutti come Lock/ synchronizedecc.


-1

Un altro semplice esempio è quello di effettuare una modifica thread sicura in un oggetto sessione.

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;
    } 
}

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.