Es el!= comprobar hilo seguro?


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

Pero es comprobar la referencia consigo mismo una operación segura de hilo?

a != a //is this thread-safe

Traté de programar esto y usar varios hilos, pero no falló. Supongo que no podí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

Como mi programa comienza entre algunas iteraciones obtengo el valor de la bandera 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 salida true.

Como sugiere la salida después de algunas n iteraciones (no fijas), la salida parece ser un valor constante y no cambia.

Salida:

Para algunos 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
Author: Narendra Pathai, 2013-08-27

8 answers

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 vars locales dos veces, es una operación no atómica, si a fue cambiado en el medio por otra comparación de subprocesos puede producir false.

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

 122
Author: Evgeniy Dorofeev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-15 12:57:05

¿El hilo check a != a es seguro?

Si a puede ser potencialmente actualizado por otro hilo (sin la sincronización adecuada!), entonces No.

Traté de programar esto y usar varios subprocesos, pero no fallé. Supongo que no podía simular la carrera en mi máquina.

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

¿Significa esto que a != a podría devolver true.

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

Alternativamente, a != a podría devolver false a pesar de que a estaba cambiando simultáneamente.


En cuanto a la " extraña comportamiento":

Como mi programa se inicia entre algunas iteraciones obtengo el valor de la bandera 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 ejecutar el programa durante mucho tiempo no genera una sola salida verdadera.

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

  1. El programa se carga y se inicia la JVM interpretando los bytecodes. Dado que (como hemos visto en la salida javap) el bytecode hace dos cargas, (aparentemente) ves los resultados de la condición de carrera, de vez en cuando.

  2. Después de un tiempo, el código es compilado por el compilador JIT. El JIT optimizer 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 esto es todo consistente con lo que el JLS permite hacer una implementación de Java.


@kriss comentó así:

Esto parece que esto podría ser lo que los programadores de C o C++ llaman "Comportamiento indefinido" (dependiente de la implementación). Parece que podría haber algunos UB en Java en casos de esquina como este una.

El Modelo de Memoria Java (especificado en JLS 17.4) especifica un conjunto de precondiciones bajo las cuales se garantiza que un subproceso vea los valores de memoria escritos por otro subproceso. Si un hilo intenta leer una variable escrita por otro, y esas condiciones previas no se cumplen, entonces puede haber un número de ejecuciones posibles ... algunos de los cuales probablemente sean incorrectos (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á.

Al compilador se le permite 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 sincronice correctamente (y por lo tanto las relaciones "sucede antes" no restringen suficientemente el conjunto de ejecuciones bien formadas) el compilador puede reordenar las cargas y los almacenes de manera que den resultados "incorrectos". (Pero eso es realmente solo decir que el programa es incorrecto.)

 45
Author: Stephen C,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-13 04:52:17

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 , es NO hilo seguro

 27
Author: Arnaud Denoyelle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-27 08:48:04

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

La máquina virtual Java puede:

  1. Lea " a " dos veces, ponga cada uno en la pila y luego compare los resultados
  2. Lea "a" solo una vez, póngalo en la pila, duplíquelo (instrucción" dup") y ejecute la comparación
  3. Elimine la expresión completamente y reemplácela con false

En el 1er caso, otro hilo podría modificar el valor de "a" entre las dos lecturas.

La estrategia elegida depende del compilador 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 "media barrera de memoria") o agregar una barrera de memoria completa (synchronized). También puede usar alguna API de nivel hgiher (por ejemplo, AtomicInteger como mencionó Juned Ahasan).

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

 15
Author: stefan.schwetschke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-27 08:42:47

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:

-XX:InlineSmallCode=0

Esto debería evitar la optimización realizada por el JIT (lo hace en el servidor hotspot 7) y verás true para siempre (me detuve en 2,000,000 pero supongo que continúa después de eso).

Para información, a continuación se muestra el código JIT'ed. Para ser honesto, no leo el ensamblaje con la fluidez suficiente para saber si la prueba se realiza realmente o dónde vienen las dos cargas de. (la línea 26 es la prueba flag = a != a y la línea 31 es la llave de cierre de la 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   
 6
Author: assylias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-27 14:56:16

No, a != a no es seguro para hilos. Esta expresión consta de tres partes: load a, load a again, y perform !=. Es posible que otro hilo obtenga el bloqueo intrínseco en el padre de a y cambie el valor de a entre las 2 operaciones de carga.

Otro factor sin embargo 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 el subproceso.

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

También debe imprimir siempre false.

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

 5
Author: DoubleMx2,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-27 20:17:11

Con respecto al comportamiento extraño:

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

 3
Author: Walter Laan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-27 10:41:25

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

 0
Author: ZhekaKozlov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-23 08:20:56