Es importante comprender que la seguridad de los hilos tiene dos aspectos.
- control de ejecución, y
- visibilidad de memoria
El primero tiene que ver con controlar cuándo se ejecuta el código (incluido el orden en que se ejecutan las instrucciones) y si puede ejecutarse simultáneamente, y el segundo tiene que ver con cuándo los efectos en la memoria de lo que se ha hecho son visibles para otros hilos. Debido a que cada CPU tiene varios niveles de caché entre ella y la memoria principal, los subprocesos que se ejecutan en diferentes CPU o núcleos pueden ver la "memoria" de manera diferente en cualquier momento dado porque los subprocesos pueden obtener y trabajar en copias privadas de la memoria principal.
El uso synchronized
evita que cualquier otro hilo obtenga el monitor (o bloqueo) para el mismo objeto , evitando así que todos los bloques de código protegidos por sincronización en el mismo objeto se ejecuten simultáneamente. La sincronización también crea una barrera de memoria "sucede antes", lo que provoca una restricción de visibilidad de la memoria de tal manera que cualquier cosa que se haga hasta el punto en que un subproceso libera un bloqueo aparece en otro subproceso que posteriormente adquiere el mismo bloqueo antes de adquirir el bloqueo. En términos prácticos, en el hardware actual, esto generalmente provoca el enjuague de los cachés de la CPU cuando se adquiere un monitor y se escribe en la memoria principal cuando se libera, los cuales son (relativamente) caros.
El uso volatile
, por otro lado, obliga a todos los accesos (lectura o escritura) a la variable volátil a ocurrir en la memoria principal, manteniendo efectivamente la variable volátil fuera de los cachés de la CPU. Esto puede ser útil para algunas acciones en las que simplemente se requiere que la visibilidad de la variable sea correcta y el orden de acceso no sea importante. El uso volatile
también cambia el tratamiento long
y double
requiere que los accesos a ellos sean atómicos; en algunos hardware (más antiguo) esto podría requerir bloqueos, aunque no en el hardware moderno de 64 bits. Bajo el nuevo modelo de memoria (JSR-133) para Java 5+, la semántica de volátil se ha fortalecido para ser casi tan fuerte como sincronizada con respecto a la visibilidad de la memoria y el orden de las instrucciones (ver http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile) Para fines de visibilidad, cada acceso a un campo volátil actúa como la mitad de una sincronización.
Bajo el nuevo modelo de memoria, todavía es cierto que las variables volátiles no pueden reordenarse entre sí. La diferencia es que ahora ya no es tan fácil reordenar los accesos de campo normales a su alrededor. Escribir en un campo volátil tiene el mismo efecto de memoria que un lanzamiento de monitor, y leer desde un campo volátil tiene el mismo efecto de memoria que adquiere un monitor. En efecto, debido a que el nuevo modelo de memoria impone restricciones más estrictas al reordenamiento de los accesos de campo volátiles con otros accesos de campo, volátiles o no, todo lo que era visible para el hilo A
cuando escribe en el campo volátil se f
vuelve visible para el hilo B
cuando lee f
.
- Preguntas frecuentes sobre JSR 133 (modelo de memoria Java)
Entonces, ahora ambas formas de barrera de memoria (bajo el JMM actual) causan una barrera de reordenamiento de instrucciones que evita que el compilador o el tiempo de ejecución reordene las instrucciones a través de la barrera. En el antiguo JMM, la volatilidad no impedía reordenar. Esto puede ser importante, porque aparte de las barreras de memoria, la única limitación impuesta es que, para cualquier subproceso en particular , el efecto neto del código es el mismo que sería si las instrucciones se ejecutaran exactamente en el orden en que aparecen en el fuente.
Un uso de volátil es que un objeto compartido pero inmutable se recrea sobre la marcha, con muchos otros hilos tomando una referencia al objeto en un punto particular de su ciclo de ejecución. Uno necesita que los otros subprocesos comiencen a usar el objeto recreado una vez que se publica, pero no necesita la sobrecarga adicional de la sincronización completa y su contención auxiliar y el vaciado de caché.
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
Hablando a su pregunta de lectura-actualización-escritura, específicamente. Considere el siguiente código inseguro:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
Ahora, con el método updateCounter () no sincronizado, pueden ingresar dos subprocesos al mismo tiempo. Entre las muchas permutaciones de lo que podría suceder, una es que el subproceso-1 realiza la prueba para el contador == 1000 y lo encuentra verdadero y luego se suspende. Entonces thread-2 hace la misma prueba y también lo ve verdadero y se suspende. Luego se reanuda el subproceso 1 y se establece el contador en 0. Luego se reanuda el subproceso 2 y nuevamente se establece el contador en 0 porque se perdió la actualización del subproceso 1. Esto también puede suceder incluso si el cambio de subproceso no ocurre como lo he descrito, sino simplemente porque dos copias diferentes de contador en caché estaban presentes en dos núcleos de CPU diferentes y cada subproceso se ejecutó en un núcleo separado. Para el caso, un hilo podría tener un contador en un valor y el otro podría tener un contador en un valor completamente diferente solo por el almacenamiento en caché.
Lo importante en este ejemplo es que el contador variable se leyó de la memoria principal a la memoria caché, se actualizó en la memoria caché y solo se volvió a escribir en la memoria principal en algún momento indeterminado más tarde, cuando se produjo una barrera de memoria o cuando la memoria caché era necesaria para otra cosa. Hacer el contador volatile
es insuficiente para la seguridad del hilo de este código, porque la prueba para el máximo y las asignaciones son operaciones discretas, incluido el incremento que es un conjunto de read+increment+write
instrucciones de máquina no atómicas , algo como:
MOV EAX,counter
INC EAX
MOV counter,EAX
Las variables volátiles son útiles solo cuando todas las operaciones realizadas en ellas son "atómicas", como en mi ejemplo en el que una referencia a un objeto completamente formado solo se lee o se escribe (y, de hecho, normalmente solo se escribe desde un único punto). Otro ejemplo sería una referencia de matriz volátil que respalde una lista de copia en escritura, siempre que la matriz solo se lea tomando primero una copia local de la referencia.
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
fuente
synchronized
es un modificador de restricción de acceso a nivel de bloque / nivel de método. Se asegurará de que un hilo posea el bloqueo para la sección crítica. Solo el hilo, que posee una cerradura, puede ingresar alsynchronized
bloque. Si otros hilos intentan acceder a esta sección crítica, deben esperar hasta que el propietario actual libere el bloqueo.volatile
es un modificador de acceso variable que obliga a todos los hilos a obtener el último valor de la variable de la memoria principal. No se requiere bloqueo para acceder a lasvolatile
variables. Todos los hilos pueden acceder al valor variable volátil al mismo tiempo.Un buen ejemplo para usar variable volátil:
Date
variable.Suponga que ha hecho que la Fecha sea variable
volatile
. Todos los subprocesos, que acceden a esta variable, siempre obtienen los datos más recientes de la memoria principal para que todos los subprocesos muestren un valor de fecha real (real). No necesita hilos diferentes que muestren tiempos diferentes para la misma variable. Todos los hilos deben mostrar el valor de fecha correcto.Eche un vistazo a este artículo para comprender mejor el
volatile
concepto.Lawrence Dol explicó claramente tu
read-write-update query
.En cuanto a sus otras consultas
volatile
Debe usarlo si cree que todos los hilos deberían obtener el valor real de la variable en tiempo real como en el ejemplo que he explicado para la variable Fecha.La respuesta será la misma que en la primera consulta.
Consulte este artículo para una mejor comprensión.
fuente
tl; dr :
Hay 3 problemas principales con multihilo:
1) Condiciones de carrera
2) Caché / memoria obsoleta
3) Cumplidor y optimizaciones de CPU
volatile
puede resolver 2 y 3, pero no puede resolver 1.synchronized
/ los bloqueos explícitos pueden resolver 1, 2 y 3.Elaboración :
1) Considere este código inseguro de hilo:
x++;
Si bien puede parecer una operación, en realidad es 3: leer el valor actual de x de la memoria, agregarle 1 y guardarlo nuevamente en la memoria. Si pocos subprocesos intentan hacerlo al mismo tiempo, el resultado de la operación no está definido. Si
x
originalmente era 1, después de que 2 hilos operen el código, puede ser 2 y puede ser 3, dependiendo de qué hilo completó qué parte de la operación antes de que el control se transfiriera al otro hilo. Esta es una forma de condición de carrera .El uso
synchronized
de un bloque de código lo hace atómico , lo que significa que las 3 operaciones suceden a la vez, y no hay forma de que otro hilo se interponga en el medio. Así que six
era 1 y 2 hilos tratan de preformasx++
que sabemos que al final será igual a 3. Por lo que resuelve el problema de condición de carrera.Calificación
x
comovolatile
no hacex++;
atómico, por lo que no resuelve este problema.2) Además, los subprocesos tienen su propio contexto, es decir, pueden almacenar en caché los valores de la memoria principal. Eso significa que algunos subprocesos pueden tener copias de una variable, pero operan en su copia de trabajo sin compartir el nuevo estado de la variable entre otros subprocesos.
Tengamos en cuenta que en un hilo,
x = 10;
. Y un poco más tarde, en otro hilo,x = 20;
. Es posible que el cambio en el valor dex
no aparezca en el primer subproceso, porque el otro subproceso ha guardado el nuevo valor en su memoria de trabajo, pero no lo ha copiado en la memoria principal. O que lo copió a la memoria principal, pero el primer hilo no ha actualizado su copia de trabajo. Entonces, si ahora el primer hilo verificaif (x == 20)
la respuesta seráfalse
.Marcar una variable como
volatile
básicamente le dice a todos los hilos que hagan operaciones de lectura y escritura solo en la memoria principal.synchronized
le dice a cada subproceso que actualice su valor de la memoria principal cuando ingresan al bloque y que devuelva el resultado a la memoria principal cuando salga del bloque.Tenga en cuenta que, a diferencia de las carreras de datos, la memoria obsoleta no es tan fácil de (re) producir, ya que de todos modos se producen descargas en la memoria principal.
3) El compilador y la CPU pueden (sin ninguna forma de sincronización entre subprocesos) tratar todo el código como subproceso único. Lo que significa que puede ver algún código, que es muy significativo en un aspecto de subprocesos múltiples, y tratarlo como si fuera un solo subproceso, donde no es tan significativo. Por lo tanto, puede mirar un código y decidir, en aras de la optimización, reordenarlo o incluso eliminar partes de él por completo, si no sabe que este código está diseñado para funcionar en múltiples hilos.
Considere el siguiente código:
Pensaría que threadB solo podría imprimir 20 (o no imprimir nada en absoluto si se ejecuta threadB if-check antes de establecerlo
b
en verdadero), ya queb
se establece en verdadero solo después de quex
se establece en 20, pero el compilador / CPU podría decidir reordenar threadA, en ese caso, threadB también podría imprimir 10. Marcadob
comovolatile
asegura que no se reordenará (o descartará en ciertos casos). Lo que significa que threadB solo podría imprimir 20 (o nada en absoluto). Marcar los métodos como sincronizados logrará el mismo resultado. También marcando una variable comovolatile
solo asegura que no se reordenará, pero todo antes / después aún se puede reordenar, por lo que la sincronización puede ser más adecuada en algunos escenarios.Tenga en cuenta que antes de Java 5 New Memory Model, volatile no resolvía este problema.
fuente
INC
operación de ensamblaje , las operaciones subyacentes de la CPU siguen siendo 3 veces mayores y requieren bloqueo para la seguridad del hilo. Buen punto. Aunque, losINC/DEC
comandos se pueden marcar atómicamente en el ensamblaje y seguir siendo 1 operación atómica.