¿Cuándo usar AtomicReference en Java?

311

¿Cuándo usamos AtomicReference?

¿Es necesario crear objetos en todos los programas multiproceso?

Proporcione un ejemplo simple donde se debe usar AtomicReference.

Chintu
fuente

Respuestas:

215

La referencia atómica debe usarse en un entorno en el que necesite realizar operaciones atómicas simples (es decir , seguras para subprocesos , no triviales) en una referencia, para la cual la sincronización basada en monitor no es adecuada. Suponga que desea verificar si un campo específico solo si el estado del objeto permanece como lo comprobó por última vez:

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

Debido a la semántica de referencia atómica, puede hacerlo incluso si el cacheobjeto se comparte entre subprocesos, sin usar synchronized. En general, es mejor usar sincronizadores o el java.util.concurrentmarco en lugar de estar desnudo a Atomic*menos que sepa lo que está haciendo.

Dos excelentes referencias de árbol muerto que le presentarán este tema:

Tenga en cuenta que (no sé si esto siempre ha sido cierto) la asignación de referencia (es decir =) es atómica en sí misma (actualizar tipos primitivos de 64 bits como longo doubleno puede ser atómica; pero actualizar una referencia siempre es atómica, incluso si es de 64 bits ) sin usar explícitamente un Atomic*.
Consulte la Especificación del lenguaje Java 3ed, Sección 17.7 .

andersoj
fuente
43
Corrígeme si me equivoco, pero parece que la clave para necesitar esto es porque necesitas hacer un "compareAndSet". Si todo lo que necesitaba hacer fuera configurar, ¿no necesitaría el AtomicObject en absoluto debido a que las actualizaciones de referencia en sí mismas son atómicas?
sMoZely
¿Es seguro hacer cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate))? Es decir, en línea el cálculo?
kaqqao
44
@veggen Los argumentos de función en Java se evalúan antes que la función misma, por lo que la alineación no hace ninguna diferencia en este caso. Sí, es seguro
Dmitry
29
@sMoZely Eso es correcto, pero si no está usando AtomicReference, debe marcar la variable volatileporque, si bien el tiempo de ejecución garantiza que la asignación de referencia es atómica, el compilador puede realizar optimizaciones bajo el supuesto de que la variable no estaba siendo modificada por otros subprocesos.
kbolino
1
@BradCupit nota que dice "si usted está no usar AtomicReference"; si lo está utilizando, mi consejo sería ir en la dirección opuesta y marcarlo finalpara que el compilador pueda optimizarlo en consecuencia.
kbolino
91

Una referencia atómica es ideal para usar cuando necesita compartir y cambiar el estado de un objeto inmutable entre múltiples hilos. Esa es una declaración súper densa, así que la desglosaré un poco.

Primero, un objeto inmutable es un objeto que efectivamente no cambia después de la construcción. Con frecuencia, los métodos de un objeto inmutable devuelven nuevas instancias de esa misma clase. Algunos ejemplos incluyen las clases de contenedor de Long y Double, así como String, solo por nombrar algunos. (De acuerdo con la concurrencia de programación en la JVM, los objetos inmutables son una parte crítica de la concurrencia moderna).

A continuación, por qué AtomicReference es mejor que un objeto volátil para compartir ese valor compartido. Un ejemplo de código simple mostrará la diferencia.

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

Cada vez que desee modificar la cadena a la que hace referencia ese campo volátil en función de su valor actual, primero debe obtener un bloqueo en ese objeto. Esto evita que algún otro hilo entre mientras tanto y cambie el valor en el medio de la nueva concatenación de cadenas. Luego, cuando se reanuda su hilo, golpea el trabajo del otro hilo. Pero, sinceramente, ese código funcionará, se ve limpio y haría feliz a la mayoría de las personas.

Pequeño problema. Es lento. Especialmente si hay mucha contención de ese Objeto de bloqueo. Esto se debe a que la mayoría de los bloqueos requieren una llamada al sistema operativo, y su subproceso se bloqueará y se cambiará el contexto de la CPU para dar paso a otros procesos.

La otra opción es usar una referencia atómica.

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

Ahora, ¿por qué es esto mejor? Honestamente, ese código es un poco menos limpio que antes. Pero hay algo realmente importante que sucede debajo del capó en AtomicRefrence, y es comparar e intercambiar. Es una sola instrucción de CPU, no una llamada del sistema operativo, lo que hace que el cambio ocurra. Esa es una sola instrucción en la CPU. Y debido a que no hay bloqueos, no hay cambio de contexto en el caso de que el bloqueo se ejerza, lo que ahorra aún más tiempo.

El problema es que, para AtomicReferences, esto no utiliza una llamada .equals (), sino una comparación == para el valor esperado. Así que asegúrese de que lo esperado sea el objeto real devuelto por get in the loop.

Erik Helleren
fuente
14
Sus dos ejemplos se comportan de manera diferente. Tendría workedque seguir para obtener la misma semántica.
CurtainDog
55
Creo que debería inicializar el valor dentro del constructor AtomicReference, de lo contrario, otro hilo aún puede ver el valor nulo antes de llamar a shared.set. (A menos que shared.set se ejecute en un inicializador estático).
Henno Vermeulen
8
En su segundo ejemplo, a partir de Java 8 debería usar algo como: shared.updateAndGet ((x) -> (x + "vamos a agregar algo")); ... que repetidamente llamará a .compareAndSet hasta que funcione. Eso es equivalente al bloque sincronizado que siempre tendría éxito. Sin embargo, debe asegurarse de que la lambda que pasa no tenga efectos secundarios porque puede llamarse varias veces.
Tom Dibble
2
No es necesario hacer String volátil sharedValue. El sincronizado (bloqueo) es lo suficientemente bueno como para establecer que suceda antes de la relación.
Jai Pandit
2
"... cambiar el estado de un objeto inmutable" es impreciso aquí, en parte porque al ser literal no se puede cambiar el estado de un objeto inmutable. El ejemplo muestra cómo cambiar la referencia de una instancia de objeto inmutable a otra diferente. Me doy cuenta de que es pedante, pero creo que vale la pena resaltarlo dado lo confuso que puede ser la lógica de Thread.
Mark Phillips el
30

Aquí hay un caso de uso para AtomicReference:

Considere esta clase que actúa como un rango de números y utiliza variables AtmomicInteger individuales para mantener los límites numéricos inferior y superior.

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

Tanto setLower como setUpper son secuencias de verificar y luego actuar, pero no usan suficiente bloqueo para hacerlas atómicas. Si el rango de números se mantiene (0, 10), y un subproceso llama a setLower (5) mientras que otro subproceso llama a setUpper (4), con un tiempo desafortunado, ambos pasarán las verificaciones en los setters y se aplicarán ambas modificaciones. El resultado es que el rango ahora contiene (5, 4) un estado no válido. Entonces, mientras que los AtomicIntegers subyacentes son seguros para subprocesos, la clase compuesta no lo es. Esto se puede solucionar mediante el uso de AtomicReference en lugar de utilizar AtomicIntegers individuales para los límites superior e inferior.

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
fuente
2
Este artículo es similar a su respuesta, pero profundiza en cosas más complicadas. ¡Es interesante! ibm.com/developerworks/java/library/j-jtp04186
LppEdd
20

Puede usar AtomicReference al aplicar bloqueos optimistas. Tiene un objeto compartido y desea cambiarlo desde más de 1 hilo.

  1. Puedes crear una copia del objeto compartido
  2. Modificar el objeto compartido
  3. Debe comprobar que el objeto compartido sigue siendo el mismo que antes; en caso afirmativo, actualice con la referencia de la copia modificada.

Como otro hilo podría haberlo modificado y / o puede modificar entre estos 2 pasos. Necesitas hacerlo en una operación atómica. aquí es donde AtomicReference puede ayudar

HamoriZ
fuente
7

Aquí hay un caso de uso muy simple y no tiene nada que ver con la seguridad del hilo.

Para compartir un objeto entre invocaciones lambda, AtomicReferencees una opción :

public void doSomethingUsingLambdas() {

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

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

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

No estoy diciendo que este sea un buen diseño ni nada (es solo un ejemplo trivial), pero si tiene el caso en el que necesita compartir un objeto entre invocaciones lambda, AtomicReferencees una opción.

De hecho, puede usar cualquier objeto que contenga una referencia, incluso una Colección que solo tenga un elemento. Sin embargo, la referencia atómica es perfecta.

Benny Bottema
fuente
6

No hablaré mucho Ya mis respetados compañeros amigos han dado su valiosa aportación. El código de ejecución completo al final de este blog debería eliminar cualquier confusión. Se trata de un pequeño programa de reserva de asientos de cine en un escenario de subprocesos múltiples.

Algunos hechos elementales importantes son los siguientes. 1> Diferentes subprocesos solo pueden competir, por ejemplo, con variables miembro estáticas en el espacio de almacenamiento dinámico. 2> La lectura o escritura volátil es completamente atómica y serializada / ocurre antes y solo se realiza desde la memoria. Al decir esto quiero decir que cualquier lectura seguirá a la escritura anterior en la memoria. Y cualquier escritura seguirá la lectura anterior de la memoria. Por lo tanto, cualquier hilo que trabaje con un volátil siempre verá el valor más actualizado. AtomicReference utiliza esta propiedad de volátil.

Los siguientes son algunos de los códigos fuente de AtomicReference. AtomicReference se refiere a una referencia de objeto. Esta referencia es una variable miembro volátil en la instancia de AtomicReference como se muestra a continuación.

private volatile V value;

get () simplemente devuelve el último valor de la variable (como lo hacen los volátiles de una manera "ocurre antes").

public final V get()

El siguiente es el método más importante de referencia atómica.

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

El método compareAndSet (esperar, actualizar) llama al método compareAndSwapObject () de la clase insegura de Java. Este método de llamada de inseguro invoca la llamada nativa, que invoca una sola instrucción para el procesador. "esperar" y "actualizar" cada referencia a un objeto.

Si y solo si el "valor" de la variable de miembro de instancia AtomicReference se refiere al mismo objeto al que se hace referencia por "esperar", ahora se asigna "actualización" a esta variable de instancia y se devuelve "verdadero". O bien, se devuelve falso. Todo se hace atómicamente. Ningún otro hilo puede interceptar en el medio. Como se trata de una operación de procesador único (magia de la arquitectura moderna de la computadora), a menudo es más rápido que usar un bloque sincronizado. Pero recuerde que cuando varias variables necesitan actualizarse atómicamente, AtomicReference no ayudará.

Me gustaría agregar un código de ejecución completo, que se puede ejecutar en eclipse. Despejaría mucha confusión. Aquí 22 usuarios (hilos MyTh) están tratando de reservar 20 asientos. A continuación se muestra el fragmento de código seguido del código completo.

Fragmento de código donde 22 usuarios intentan reservar 20 asientos.

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

El siguiente es el código de ejecución completo.

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
fuente
5

¿Cuándo usamos la referencia atómica?

AtomicReference es una forma flexible de actualizar el valor de la variable atómicamente sin el uso de sincronización.

AtomicReference admite programación segura y sin hilos de bloqueo en variables individuales.

Hay varias formas de lograr la seguridad de subprocesos con API concurrente de alto nivel . Las variables atómicas son una de las múltiples opciones.

Lock Los objetos admiten modismos de bloqueo que simplifican muchas aplicaciones concurrentes.

Executorsdefinir una API de alto nivel para lanzar y administrar hilos. Las implementaciones de ejecutor proporcionadas por java.util.concurrent proporcionan una gestión de grupo de subprocesos adecuada para aplicaciones a gran escala.

Las colecciones simultáneas facilitan la gestión de grandes colecciones de datos y pueden reducir en gran medida la necesidad de sincronización.

Las variables atómicas tienen características que minimizan la sincronización y ayudan a evitar errores de consistencia de la memoria.

Proporcione un ejemplo simple donde se debe usar AtomicReference.

Código de muestra 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);

¿Es necesario crear objetos en todos los programas multiproceso?

No tiene que usarlo AtomicReferenceen todos los programas de subprocesos múltiples.

Si desea proteger una sola variable, use AtomicReference. Si desea proteger un bloque de código, use otras construcciones como Lock/ synchronizedetc.

Ravindra babu
fuente
-1

Otro ejemplo simple es hacer una modificación de hilo seguro en un objeto de sesión.

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

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

Dherik
fuente