Sé que las operaciones compuestas, como i++
no son seguras para subprocesos, ya que implican múltiples operaciones.
¿Pero verificar la referencia consigo mismo es una operación segura para subprocesos?
a != a //is this thread-safe
Traté de programar esto y usar múltiples hilos pero no falló. Supongo que no podría simular la carrera en mi máquina.
EDITAR:
public class TestThreadSafety {
private Object a = new Object();
public static void main(String[] args) {
final TestThreadSafety instance = new TestThreadSafety();
Thread testingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
long countOfIterations = 0L;
while(true){
boolean flag = instance.a != instance.a;
if(flag)
System.out.println(countOfIterations + ":" + flag);
countOfIterations++;
}
}
});
Thread updatingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
instance.a = new Object();
}
}
});
testingReferenceThread.start();
updatingReferenceThread.start();
}
}
Este es el programa que estoy usando para probar la seguridad del hilo.
Comportamiento extraño
Cuando mi programa comienza entre algunas iteraciones, obtengo el valor del indicador de salida, lo que significa que la !=
verificación de referencia falla en la misma referencia. PERO después de algunas iteraciones, la salida se convierte en un valor constante false
y luego ejecutar el programa durante mucho tiempo no genera una sola true
salida.
Como sugiere la salida después de algunas iteraciones n (no fijas), la salida parece ser un valor constante y no cambia.
Salida:
Para algunas iteraciones:
1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true
fuente
1234:true
nunca se rompen entre sí ). Una prueba de carrera necesita un circuito interno más ajustado. Imprima un resumen al final (como alguien hizo a continuación con un marco de prueba de unidad).Respuestas:
En ausencia de sincronización este código
puede producir
true
. Este es el bytecode paratest()
Como podemos ver, carga el campo
a
a los vars locales dos veces, es una operación no atómica, sia
se produce un cambio en el medio por otra comparación de subprocesosfalse
.Además, el problema de visibilidad de la memoria es relevante aquí, no hay garantía de que los cambios
a
realizados por otro hilo sean visibles para el hilo actual.fuente
!=
, que implica cargar el LHS y el RHS por separado. Entonces, si el JLS no menciona nada específico sobre optimizaciones cuando LHS y RHS son sintácticamente idénticos, entonces se aplicaría la regla general, lo que significa cargara
dos veces.Si
a
potencialmente puede ser actualizado por otro hilo (¡sin la sincronización adecuada!), Entonces No.¡Eso no significa nada! El problema es que si el JLS permite una ejecución en la que
a
otro subproceso actualiza , entonces el código no es seguro para subprocesos. El hecho de que no se pueda hacer que la condición de carrera suceda con un caso de prueba particular en una máquina particular y una implementación de Java particular, no impide que ocurra en otras circunstancias.Sí, en teoría, bajo ciertas circunstancias.
Alternativamente,
a != a
podría regresarfalse
aunquea
estuviera cambiando simultáneamente.Sobre el "comportamiento extraño":
Este comportamiento "extraño" es consistente con el siguiente escenario de ejecución:
El programa se carga y la JVM comienza a interpretar los códigos de bytes. Como (como hemos visto en la salida de javap) el código de bytes realiza dos cargas, usted (aparentemente) ve los resultados de la condición de carrera, ocasionalmente.
Después de un tiempo, el compilador JIT compila el código. El optimizador JIT se da cuenta de que hay dos cargas de la misma ranura de memoria (
a
) juntas, y optimiza la segunda. (De hecho, existe la posibilidad de que optimice la prueba por completo ...)Ahora la condición de carrera ya no se manifiesta, porque ya no hay dos cargas.
Tenga en cuenta que esto es todo coherente con lo que el JLS permite una implementación de Java para hacer.
@kriss comentó así:
El modelo de memoria Java (especificado en JLS 17.4 ) especifica un conjunto de condiciones previas bajo las cuales se garantiza que un hilo vea los valores de memoria escritos por otro hilo. Si un hilo intenta leer una variable escrita por otro, y esas condiciones previas no se cumplen, entonces puede haber varias ejecuciones posibles ... algunas de las cuales probablemente sean incorrectas (desde la perspectiva de los requisitos de la aplicación). En otras palabras, el conjunto de posibles comportamientos (es decir, el conjunto de "ejecuciones bien formadas") está definido, pero no podemos decir cuál de esos comportamientos ocurrirá.
El compilador puede combinar y reordenar cargas y guardar (y hacer otras cosas) siempre que el efecto final del código sea el mismo:
Pero si el código no se sincroniza correctamente (y, por lo tanto, las relaciones "suceden antes" no limitan suficientemente el conjunto de ejecuciones bien formadas), el compilador puede reordenar cargas y almacenes de manera que arrojarían resultados "incorrectos". (Pero eso realmente solo dice que el programa es incorrecto).
fuente
a != a
podría volver cierto?Probado con test-ng:
Tengo 2 fallas en 10 000 invocaciones. Así que NO , se NO hilo de seguridad
fuente
Random.nextInt()
parte es superflua. Podrías haber probado connew Object()
igual de bien.No, no es. Para una comparación, la máquina virtual Java debe poner los dos valores para comparar en la pila y ejecutar la instrucción de comparación (cuál depende del tipo de "a").
La máquina virtual Java puede:
false
En el primer caso, otro hilo podría modificar el valor de "a" entre las dos lecturas.
La estrategia elegida depende del compilador de Java y del Java Runtime (especialmente el compilador JIT). Incluso puede cambiar durante el tiempo de ejecución de su programa.
Si desea asegurarse de cómo se accede a la variable, debe hacerlo
volatile
(una llamada "barrera de memoria media") o agregar una barrera de memoria completa (synchronized
). También puede usar alguna API de nivel superior (por ejemplo,AtomicInteger
como mencionó Juned Ahasan).Para obtener detalles sobre la seguridad de subprocesos, lea JSR 133 ( Modelo de memoria Java ).
fuente
a
comovolatile
sería todavía implicar dos distintos lee, con la posibilidad de un cambio en el medio.Todo ha sido bien explicado por Stephen C. Por diversión, podría intentar ejecutar el mismo código con los siguientes parámetros de JVM:
Esto debería evitar la optimización realizada por el JIT (lo hace en el servidor de punto de acceso 7) y verá
true
para siempre (me detuve en 2,000,000 pero supongo que continúa después de eso).Para obtener información, a continuación se muestra el código JIT. Para ser honesto, no leo el ensamblaje con suficiente fluidez como para saber si la prueba realmente se realizó o de dónde provienen las dos cargas. (la línea 26 es la prueba
flag = a != a
y la línea 31 es la llave de cierre de lawhile(true)
).fuente
0x27dccd1
a0x27dccdf
. Eljmp
en el bucle es incondicional (ya que el bucle es infinito). Las únicas otras dos instrucciones en el bucle sonadd rbc, 0x1
: que se está incrementandocountOfIterations
(a pesar de que el bucle nunca se cerrará, por lo que este valor no se leerá: tal vez sea necesario en caso de que lo interrumpa en el depurador), ... .test
instrucción de aspecto extraño , que en realidad solo está disponible para el acceso a la memoria (¡tenga en cuenta queeax
nunca se establece en el método!): es una página especial que se configura como no legible cuando la JVM desea activar todos los hilos para llegar a un punto seguro, para que pueda hacer gc o alguna otra operación que requiera que todos los hilos estén en un estado conocido.instance. a != instance.a
comparación del bucle, y solo la realiza una vez, ¡antes de ingresar al bucle! Sabe que no es necesario volver a cargarinstance
oa
que no se declaran volátiles y que no hay otro código que pueda cambiarlos en el mismo subproceso, por lo que solo supone que son iguales durante todo el ciclo, lo cual está permitido por la memoria modelo.No,
a != a
no es seguro para subprocesos. Esta expresión consta de tres partes: cargara
, cargar dea
nuevo y realizar!=
. Es posible que otro subproceso obtenga el bloqueo intrínseco ena
el padre y cambie el valor dea
entre las 2 operaciones de carga.Sin embargo, otro factor es si
a
es local. Sia
es local, entonces ningún otro subproceso debería tener acceso y, por lo tanto, debería ser seguro para subprocesos.También debe imprimir siempre
false
.Declarar
a
comovolatile
no resolvería el problema sia
esstatic
o instancia. El problema no es que los hilos tengan valores diferentesa
, sino que un hilo se cargaa
dos veces con valores diferentes. En realidad, puede hacer que el caso sea menos seguro para subprocesos. Sia
no lo estávolatile
,a
puede almacenarse en caché y un cambio en otro subproceso no afectará el valor almacenado en caché.fuente
synchronized
está equivocado: para garantizar que se imprima ese códigofalse
, todos los métodos que establezca tambiéna
deberían serlosynchronized
.a
el padre mientras se ejecuta el método, necesario para establecer el valora
?Con respecto al comportamiento extraño:
Dado que la variable
a
no está marcada comovolatile
, en algún momento su valora
podría ser almacenado en caché por el hilo. Ambosa
sa != a
son entonces la versión en caché y, por lo tanto, siempre son los mismos (el significadoflag
ahora es siemprefalse
).fuente
Incluso la simple lectura no es atómica. Si
a
estálong
y no está marcado comovolatile
entonces en JVM de 32 bitslong b = a
no es seguro para subprocesos.fuente