Usted pregunta específicamente cómo funcionan internamente , así que aquí está:
Sin sincronización
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
Básicamente lee el valor de la memoria, lo incrementa y lo devuelve a la memoria. Esto funciona en un solo subproceso, pero hoy en día, en la era de los cachés de varios núcleos, múltiples CPU y niveles múltiples, no funcionará correctamente. En primer lugar, presenta la condición de carrera (varios hilos pueden leer el valor al mismo tiempo), pero también problemas de visibilidad. El valor solo puede almacenarse en la memoria de la CPU " local " (algo de caché) y no ser visible para otras CPU / núcleos (y, por lo tanto, subprocesos). Es por eso que muchos se refieren a la copia local de una variable en un hilo. Es muy inseguro. Considere este popular pero roto código de detención de subprocesos:
private boolean stopped;
public void run() {
while(!stopped) {
//do some work
}
}
public void pleaseStop() {
stopped = true;
}
Agregue volatile
a la stopped
variable y funciona bien: si cualquier otro hilo modifica la stopped
variable a través del pleaseStop()
método, se garantiza que verá ese cambio inmediatamente en el while(!stopped)
bucle del hilo de trabajo . Por cierto, esta no es una buena manera de interrumpir un hilo tampoco, vea: Cómo detener un hilo que se ejecuta para siempre sin ningún uso y Detener un hilo java específico .
AtomicInteger
private AtomicInteger counter = new AtomicInteger();
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
La AtomicInteger
clase usa operaciones de CPU de bajo nivel CAS ( comparar e intercambiar ) (¡no se necesita sincronización!) Le permiten modificar una variable particular solo si el valor presente es igual a otra cosa (y se devuelve con éxito). Entonces, cuando lo ejecutas, getAndIncrement()
en realidad se ejecuta en un bucle (implementación real simplificada):
int current;
do {
current = get();
} while(!compareAndSet(current, current + 1));
Básicamente: lee; intente almacenar valor incrementado; si no tiene éxito (el valor ya no es igual a current
), lea e intente nuevamente. El compareAndSet()
se implementa en código nativo (ensamblado).
volatile
sin sincronización
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
Este código no es correcto Soluciona el problema de visibilidad ( volatile
se asegura de que otros hilos puedan ver los cambios realizados counter
) pero aún tiene una condición de carrera. Esto se ha explicado varias veces: el incremento previo / posterior no es atómico.
El único efecto secundario volatile
es el " vaciado " de cachés para que todas las demás partes vean la versión más reciente de los datos. Esto es demasiado estricto en la mayoría de las situaciones; Por eso volatile
no es por defecto.
volatile
sin sincronización (2)
volatile int i = 0;
void incIBy5() {
i += 5;
}
El mismo problema que el anterior, pero aún peor porque i
no lo es private
. La condición de carrera aún está presente. Por qué es un problema? Si, digamos, dos hilos ejecutan este código simultáneamente, el resultado podría ser + 5
o + 10
. Sin embargo, tiene la garantía de ver el cambio.
Múltiple independiente synchronized
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Sorpresa, este código también es incorrecto. De hecho, está completamente equivocado. En primer lugar, está sincronizando i
, lo que está a punto de cambiar (además, i
es primitivo, así que supongo que está sincronizando en un temporal Integer
creado mediante autoboxing ...) Completamente defectuoso. También podrías escribir:
synchronized(new Object()) {
//thread-safe, SRSLy?
}
No pueden entrar dos hilos en el mismo synchronized
bloque con el mismo bloqueo . En este caso (y de manera similar en su código) el objeto de bloqueo cambia con cada ejecución, por lo que synchronized
efectivamente no tiene ningún efecto.
Incluso si ha utilizado una variable final (o this
) para la sincronización, el código sigue siendo incorrecto. Dos hilos pueden leer primero i
de forma temp
síncrona (con el mismo valor localmente temp
), luego el primero asigna un nuevo valor a i
(digamos, del 1 al 6) y el otro hace lo mismo (del 1 al 6).
La sincronización debe abarcar desde la lectura hasta la asignación de un valor. Su primera sincronización no tiene efecto (la lectura de un int
es atómica) y la segunda también. En mi opinión, estas son las formas correctas:
void synchronized incIBy5() {
i += 5
}
void incIBy5() {
synchronized(this) {
i += 5
}
}
void incIBy5() {
synchronized(this) {
int temp = i;
i = temp + 5;
}
}