Solución:
La referencia atómica debe usarse en un entorno en el que necesite hacer cosas simples atómico (es decir a salvo de amenazas, no triviales) operaciones en una referencia, para las cuales la sincronización basada en monitor no es apropiada. Suponga que desea verificar si un campo específico solo si el estado del objeto permanece como lo verificó 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 hacer esto incluso si el cache
el objeto se comparte entre subprocesos, sin usar synchronized
. En general, es mejor usar sincronizadores o el java.util.concurrent
marco en lugar de desnudo Atomic*
a menos que sepa lo que está haciendo.
Dos excelentes referencias de árbol muerto que le presentarán este tema:
- Herlihy es excelente El arte de la programación multiprocesador
- La concurrencia de Java en la práctica
Tenga en cuenta que (no sé si esto siempre ha sido cierto) referencia asignación (es decir =
) es en sí mismo atómico (actualización primitivo Tipos de 64 bits como long
o double
puede no ser atómico; pero actualizando un referencia es siempre atómico, 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 necesita compartir y cambiar el estado de un objeto inmutable entre varios subprocesos. Esa es una declaración superdensa, 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 contenedoras de Long y Double, así como String, solo por nombrar algunos. (De acuerdo a Programación de simultaneidad en la JVM Los objetos inmutables son una parte fundamental 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 medio de la nueva concatenación de cadenas. Luego, cuando su hilo se reanuda, aplasta el trabajo del otro hilo. Pero, sinceramente, ese código funcionará, se verá limpio y haría feliz a la mayoría de la gente.
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 hilo se bloqueará y se cambiará de contexto fuera de la CPU para dar paso a otros procesos.
La otra opción es utilizar 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);
}
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 es comparar e intercambiar. Es una sola instrucción de CPU, no una llamada al sistema operativo, lo que hace que suceda el cambio. 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 se ejerza el bloqueo, lo que ahorra aún más tiempo.
El problema es que, para AtomicReferences, esto no usa 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.
A continuación, se muestra un caso de uso de AtomicReference:
Considere esta clase que actúa como un rango de números y usa variables AtmomicInteger individuales para mantener los límites de números 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 verificar y luego actuar, pero no usan el bloqueo suficiente para hacerlas atómicas. Si el rango de números se mantiene (0, 10), y un hilo llama a setLower (5) mientras que otro hilo llama a setUpper (4), con algún tiempo desafortunado, ambos pasarán las verificaciones en los establecedores 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 usando una AtomicReference en lugar de usar 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;
}
}
}