¿Cuándo usar AtomicReference en Java?

¿Cuándo usamos AtomicReference?

¿Es necesario crear objetos en todos los progtwigs multiproceso?

Proporcione un ejemplo simple donde se debe usar AtomicReference.

La referencia atómica se debe usar en un entorno en el que necesite realizar operaciones simples atómicas (es decir, seguras para hilos , no triviales) en una referencia, para lo cual la sincronización basada en monitores no es apropiada. Supongamos que desea verificar si un campo específico solo si el estado del objeto permanece como la última vez que lo verificó:

AtomicReference cache = new AtomicReference(); 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 hacer esto incluso si el objeto de cache se comparte entre subprocesos, sin usar synchronized . En general, es mejor que uses sincronizadores o el marco java.util.concurrent lugar de Atomic* menos que sepas lo que estás haciendo.

Dos excelentes referencias de árboles muertos que le presentarán este tema:

  • La excelente progtwigción de Art of Multiprocessor de Herlihy
  • Concurrencia de Java en la práctica

Tenga en cuenta que (no sé si esto siempre ha sido cierto) la asignación de referencia (ie = ) es atómica (la actualización de tipos primitivos de 64 bits como long o double puede no ser atómica, pero la actualización de una referencia es siempre 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 .

Una referencia atómica es ideal para usar cuando necesite compartir y cambiar el estado de un objeto inmutable entre múltiples hilos. Esa es una afirmación superdensa, así que la analizaré un poco.

Primero, un objeto inmutable es un objeto que efectivamente no se 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 contenedoras de Long y Double, así como String, solo por nombrar algunas. (Según la simultaneidad de progtwigción en la JVM, los objetos inmutables son una parte crítica de la simultaneidad moderna).

A continuación, por qué AtomicReference es mejor que un objeto volátil para compartir ese valor compartido. Un simple ejemplo de código 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 locking en ese objeto. Esto evita que otros hilos entren mientras tanto y cambia el valor en el medio de la nueva concatenación de cadenas. Luego, cuando el hilo se reanuda, golpeas el trabajo del otro hilo. Pero, sinceramente, ese código funcionará, se ve limpio y haría felices a la mayoría de la gente.

Pequeño problema. Es lento. Especialmente si hay mucha contención de ese Objeto de locking. Eso es porque la mayoría de los lockings requieren una llamada al sistema operativo, y el hilo se bloqueará y se desconectará el contexto de la CPU para dar paso a otros procesos.

La otra opción es usar una AtomicRefrence.

 public static AtomicReference 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 bajo el capó en AtomicRefrence, y eso es comparar e intercambiar. Es una instrucción de CPU única, no una llamada de sistema operativo, lo que hace que el cambio ocurra. Esa es una instrucción única en la CPU. Y como no hay cerraduras, no existe un cambio de contexto en el caso de que se ejerza el locking, ¡lo que ahorra aún más tiempo!

La pega es, para AtomicReferences, esto no usa una llamada .equals (), sino una comparación == para el valor esperado. Por lo tanto, asegúrese de que lo esperado es que el objeto real devuelto se ponga al día.

Aquí hay un caso de uso para AtomicReference:

Considere esta clase que actúa como un rango numérico, y usa variables AtmomicInteger individuales para mantener los límites numéricos inferiores y superiores.

 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 verificación y acto, pero no usan suficiente locking para hacerlas atómicas. Si el rango de números se mantiene (0, 10), y un hilo llama a setLower (5) mientras otro hilo llama a setUpper (4), con un poco de mala suerte ambos pasarán las verificaciones en los setters y se aplicarán ambas modificaciones. El resultado es que el rango ahora tiene (5, 4) un estado inválido. Entonces, aunque los AtomicIntegers subyacentes son seguros para subprocesos, la clase compuesta no lo es. Esto se puede solucionar utilizando una 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 final AtomicReference values = new AtomicReference(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 } 

Puede usar AtomicReference cuando aplique lockings optimistas. Tiene un objeto compartido y desea cambiarlo de más de 1 hilo.

  1. Puede crear una copia del objeto compartido
  2. Modificar el objeto compartido
  3. Debe comprobar que el objeto compartido sigue siendo el mismo que antes; si es así, actualice con la referencia de la copia modificada.

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

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

Algunos hechos elementales importantes son los siguientes. 1> Los subprocesos diferentes solo pueden competir por ejemplo y las variables de miembro estático en el espacio de montón. 2> La lectura o escritura volátil es completamente atómica y serializada / sucede antes y solo se realiza desde la memoria. Al decir esto quiero decir que cualquier lectura seguirá la escritura anterior en la memoria. Y cualquier escritura seguirá la lectura anterior de la memoria. Entonces, cualquier hilo que trabaje con un volátil siempre verá el valor más actualizado. AtomicReference usa 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 de 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 los volátiles lo hacen de la manera “pasar antes”).

 public final V get() 

A continuación se encuentra el método más importante de AtomicReference.

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

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

Si y solo si la “variable” miembro de la instancia AtomicReference “valor” se refiere al mismo objeto se hace referencia a “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 architecture de la computadora moderna), a menudo es más rápido que usar un bloque sincronizado. Pero recuerde que cuando se deben actualizar múltiples variables atómicamente, AtomicReference no ayudará.

Me gustaría agregar un código de ejecución completo, que se puede ejecutar en eclipse. Despejaría muchas confusiones. Aquí 22 usuarios (hilos MyTh) están tratando de reservar 20 asientos. Lo siguiente es 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()); } Thread[] ths = new Thread[22];// 22 users for (int i = 0; i < ths.length; i++) { ths[i] = new MyTh(seats, i); ths[i].start(); } 

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

 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> 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()); } 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 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> l;//seats int id;//id of the users int seats; public MyTh(List> 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 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 } } } } 

¿Cuándo usamos AtomicReference?

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

AtomicReference admite la progtwigción segura de subprocesos sin locking en variables individuales.

Existen múltiples formas de lograr la seguridad del subproceso con la API concurrente de alto nivel. Las variables atómicas es una de las múltiples opciones.

Lock objetos de locking admiten expresiones de locking que simplifican muchas aplicaciones concurrentes.

Executors definen una API de alto nivel para iniciar y administrar subprocesos. Las implementaciones de Executor proporcionadas por java.util.concurrent proporcionan administración de grupo de subprocesos adecuada para aplicaciones a gran escala.

Las colecciones concurrentes facilitan la administració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 coherencia de memoria.

Proporcione un ejemplo simple donde se debe usar AtomicReference.

Código de muestra con AtomicReference :

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

¿Es necesario crear objetos en todos los progtwigs multiproceso?

No tiene que usar AtomicReference en todos los progtwigs multihilo.

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

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, AtomicReference es una opción :

 public void doSomethingUsingLambdas() { AtomicReference yourObjectRef = new AtomicReference<>(); soSomethingThatTakesALambda(() -> { yourObjectRef.set(youObject); }); soSomethingElseThatTakesALambda(() -> { YourObject yourObject = yourObjectRef.get(); }); } 

No estoy diciendo que esto 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, AtomicReference es una opción.

De hecho, puede usar cualquier objeto que tenga una referencia, incluso una Colección que solo tenga un elemento. Sin embargo, AtomicReference es un ajuste perfecto.

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 holder = (AtomicReference) ctx.getAttribute("highScore"); return holder.get(); } public void updateHighScore(PlayerScore newScore) { ServletContext ctx = getServletConfig().getServletContext(); AtomicReference holder = (AtomicReference) 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