vulnerability thread safe race español java multithreading thread-safety atomic race-condition

race - thread safe java



¿Es seguro el hilo de verificación!=? (8)

¿Es el cheque a != a hilo seguro?

Si a puede ser potencialmente actualizada por otra cadena (¡sin la sincronización adecuada!), Entonces No.

Traté de programar esto y usar varios hilos, pero no falló. Supongo que no podría simular raza en mi máquina.

Eso no significa nada! El problema es que si el JLS permite una ejecución en la que a es actualizada por otro hilo, entonces el código no es seguro para subprocesos. El hecho de que no pueda causar que la condición de carrera ocurra con un caso de prueba en particular en una máquina en particular y una implementación Java en particular, no lo impide en otras circunstancias.

¿Esto significa que a! = A podría volverse true .

Sí, en teoría, bajo ciertas circunstancias.

Alternativamente, a != a podría devolver false incluso si a estaba cambiando simultáneamente.

Con respecto al "comportamiento extraño":

Como mi programa comienza entre algunas iteraciones, obtengo el valor del indicador de salida, lo que significa que la referencia! = Check falla en la misma referencia. PERO después de algunas iteraciones, la salida se convierte en un valor constante falso y luego la ejecución del programa durante un largo tiempo no genera una única salida verdadera.

Este comportamiento "extraño" es consistente con el siguiente escenario de ejecución:

  1. 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 bytecode hace dos cargas, usted (al parecer) ve los resultados de la condición de carrera, ocasionalmente.

  2. Después de un tiempo, el código es compilado por el compilador JIT. El optimizador JIT nota 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 ...)

  3. Ahora la condición de carrera ya no se manifiesta, porque ya no hay dos cargas.

Tenga en cuenta que todo esto es coherente con lo que JLS permite que haga una implementación de Java.

@kriss comentó así:

Parece que esto podría ser lo que los programadores de C o C ++ llaman "Comportamiento no definido" (depende de la implementación). Parece que podría haber algunos UB en java en casos de esquina como este.

El Modelo de memoria de Java (especificado en JLS 17.4 ) especifica un conjunto de condiciones previas bajo las cuales se garantiza que un hilo vea 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 pueden ser incorrectas (desde la perspectiva de los requisitos de la aplicación). En otras palabras, se define el conjunto de posibles comportamientos (es decir, el conjunto de "ejecuciones bien formadas"), 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:

  • cuando se ejecuta por un solo hilo, y
  • cuando se ejecuta por diferentes hilos que se sincronizan correctamente (según el Modelo de Memoria).

Pero si el código no se sincroniza correctamente (y, por lo tanto, las relaciones "pasar antes" no limitan suficientemente el conjunto de ejecuciones bien formadas), el compilador puede reordenar cargas y almacenamientos de forma que se obtendrían resultados "incorrectos". (Pero eso solo dice que el programa es incorrecto).

Sé que las operaciones compuestas como i++ no son seguras para hilos ya que implican operaciones múltiples .

¿Pero está comprobando la referencia consigo misma una operación segura de subprocesos?

a != a //is this thread-safe

Intenté programar esto y usar varios hilos, pero no falló. Supongo que no podría simular raza 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

Como mi programa comienza entre algunas iteraciones, obtengo el valor del indicador de salida, lo que significa que la referencia != Check falla en la misma referencia. PERO después de algunas iteraciones, la salida se convierte en un valor constante false y luego la ejecución del programa durante un largo tiempo no genera una única salida true .

Como el resultado sugiere después de algunas iteraciones n (no fijas), el resultado 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


En ausencia de sincronización, este código

Object a; public boolean test() { return a != a; }

puede producir true . Este es el bytecode para test()

ALOAD 0 GETFIELD test/Test1.a : Ljava/lang/Object; ALOAD 0 GETFIELD test/Test1.a : Ljava/lang/Object; IF_ACMPEQ L1 ...

como podemos ver, carga el campo a a los vars locales dos veces, es una operación no atómica, si se cambió una en el medio por otro subproceso, la comparación puede producir false .

Además, el problema de visibilidad de la memoria es relevante aquí, no hay garantía de que los cambios hechos por otro subproceso serán visibles para el subproceso actual.


En cuanto al comportamiento extraño:

Dado que la variable a no está marcada como volatile , en algún momento el valor de a podría almacenarse en caché por el hilo. Ambas s de a != a son entonces la versión en caché y, por lo tanto, siempre iguales (lo que significa que la flag ahora siempre es false ).


Incluso la lectura simple no es atómica. Si a es long y no está marcado como volatile entonces en las JVM de 32 bits long b = a no es seguro para subprocesos.


No, a != a no es seguro para subprocesos. Esta expresión consta de tres partes: cargar a , cargar a nuevamente, y realizar != . Es posible que otro hilo gane el bloqueo intrínseco en el padre de uno y cambie el valor de a entre las 2 operaciones de carga.

Otro factor es si a es local. Si a es local, entonces ningún otro subproceso debería tener acceso a él y, por lo tanto, debería ser seguro para subprocesos.

void method () { int a = 0; System.out.println(a != a); }

también debería imprimir siempre false .

Declarar a como volatile no resolvería el problema si a es static o instancia. El problema no es que los subprocesos tengan valores diferentes de a , sino que un subproceso cargue dos veces con valores diferentes. En realidad, puede hacer que el caso sea menos seguro para subprocesos. Si a no es volatile , a puede almacenarse en caché y un cambio en otro subproceso no afectará el valor en caché.


No, no es. Para una comparación, Java VM debe poner los dos valores para comparar en la pila y ejecutar la instrucción de comparación (que depende del tipo de "a").

La VM Java puede:

  1. Lea "a" dos veces, coloque cada una en la pila y luego compare los resultados
  2. Lea "a" solo una vez, colóquelo en la pila, duplíquelo (instrucción "dup") y ejecute la comparación
  3. Elimina la expresión por completo y reemplázala por false

En el primer caso, otro subproceso podría modificar el valor de "a" entre las dos lecturas.

La estrategia que se elija depende del compilador de Java y del tiempo de ejecución de Java (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 hacerla volatile (una llamada "barrera de media memoria") o agregar una barrera de memoria completa ( synchronized ). También puede usar alguna API de nivel más alto (por ejemplo, AtomicInteger como lo menciona Juned Ahasan).

Para obtener más información sobre la seguridad del hilo, lea JSR 133 ( Modelo de memoria Java ).


Probado con test-ng:

public class MyTest { private static Integer count=1; @Test(threadPoolSize = 1000, invocationCount=10000) public void test(){ count = new Integer(new Random().nextInt()); Assert.assertFalse(count != count); } }

Tengo 2 fallas en 10 000 invocaciones. Así que NO , NO es seguro para subprocesos


Todo ha sido bien explicado por Stephen C. Por diversión, podrías tratar de ejecutar el mismo código con los siguientes parámetros de JVM:

-XX:InlineSmallCode=0

Esto debería evitar la optimización hecha por el JIT (lo hace en el servidor del punto de acceso 7) y se verá true para siempre (paré en 2,000,000 pero supongo que continúa después de eso).

Para información, a continuación está el código JIT. Para ser sincero, no leo el montaje con la suficiente fluidez como para saber si la prueba está realmente hecha o de dónde provienen las dos cargas. (la línea 26 es la flag = a != a prueba flag = a != a y la línea 31 es la llave de cierre del while(true) ).

# {method} ''run'' ''()V'' in ''javaapplication27/TestThreadSafety$1'' 0x00000000027dcc80: int3 0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0] 0x00000000027dcc8c: data32 data32 xchg ax,ax 0x00000000027dcc90: mov DWORD PTR [rsp-0x6000],eax 0x00000000027dcc97: push rbp 0x00000000027dcc98: sub rsp,0x40 0x00000000027dcc9c: mov rbx,QWORD PTR [rdx+0x8] 0x00000000027dcca0: mov rbp,QWORD PTR [rdx+0x18] 0x00000000027dcca4: mov rcx,rdx 0x00000000027dcca7: movabs r10,0x6e1a7680 0x00000000027dccb1: call r10 0x00000000027dccb4: test rbp,rbp 0x00000000027dccb7: je 0x00000000027dccdd 0x00000000027dccb9: mov r10d,DWORD PTR [rbp+0x8] 0x00000000027dccbd: cmp r10d,0xefc158f4 ; {oop(''javaapplication27/TestThreadSafety$1'')} 0x00000000027dccc4: jne 0x00000000027dccf1 0x00000000027dccc6: test rbp,rbp 0x00000000027dccc9: je 0x00000000027dcce1 0x00000000027dcccb: cmp r12d,DWORD PTR [rbp+0xc] 0x00000000027dcccf: je 0x00000000027dcce1 ;*goto ; - javaapplication27.TestThreadSafety$1::run@62 (line 31) 0x00000000027dccd1: add rbx,0x1 ; OopMap{rbp=Oop off=85} ;*goto ; - javaapplication27.TestThreadSafety$1::run@62 (line 31) 0x00000000027dccd5: test DWORD PTR [rip+0xfffffffffdb53325],eax # 0x0000000000330000 ;*goto ; - javaapplication27.TestThreadSafety$1::run@62 (line 31) ; {poll} 0x00000000027dccdb: jmp 0x00000000027dccd1 0x00000000027dccdd: xor ebp,ebp 0x00000000027dccdf: jmp 0x00000000027dccc6 0x00000000027dcce1: mov edx,0xffffff86 0x00000000027dcce6: mov QWORD PTR [rsp+0x20],rbx 0x00000000027dcceb: call 0x00000000027a90a0 ; OopMap{rbp=Oop off=112} ;*aload_0 ; - javaapplication27.TestThreadSafety$1::run@2 (line 26) ; {runtime_call} 0x00000000027dccf0: int3 0x00000000027dccf1: mov edx,0xffffffad 0x00000000027dccf6: mov QWORD PTR [rsp+0x20],rbx 0x00000000027dccfb: call 0x00000000027a90a0 ; OopMap{rbp=Oop off=128} ;*aload_0 ; - javaapplication27.TestThreadSafety$1::run@2 (line 26) ; {runtime_call} 0x00000000027dcd00: int3 ;*aload_0 ; - javaapplication27.TestThreadSafety$1::run@2 (line 26) 0x00000000027dcd01: int3