2012-01-22 9 views
9

"Concurrencia Java en la práctica" ofrece el siguiente ejemplo de una clase insegura que debido a la naturaleza del modelo de memoria java puede terminar ejecutándose o imprimir 0.Sincronización de modelo de memoria Java: ¿cómo inducir error de visibilidad de datos?

El problema que esta clase intenta demostrar es que las variables aquí no son "compartidos" entre hilos. Por lo tanto, el valor del hilo visto puede ser diferente de otro hilo, ya que no son volátiles ni están sincronizados. También debido a la reordenación de las declaraciones permitidas por la JVM ready = true, puede establecerse antes del número = 42.

Para mí esta clase siempre funciona bien usando JVM 1.6. ¿Alguna idea sobre cómo hacer que esta clase realice un comportamiento incorrecto (es decir, imprimir 0 o ejecutar para siempre)?

public class NoVisibility { 
    private static boolean ready; 
    private static int number; 

    private static class ReaderThread extends Thread { 
     public void run() { 
      while (!ready) 
       Thread.yield(); 
      System.out.println(number); 
     } 
    } 

    public static void main(String[] args) { 
     new ReaderThread().start(); 
     number = 42; 
     ready = true; 
    } 
} 
+4

Respuesta corta no. ¿Pero realmente necesita comprobar si saltar desde el techo es peligroso después de que alguien le dijo y dio buenas razones? – Voo

+1

Usa una computadora con 2 núcleos. –

+1

@Voo ... ¿De qué demonios estás hablando? –

Respuesta

6

El problema que tiene es que no está esperando el tiempo suficiente para que el código se optimice y el valor se guarde en caché.

Cuando un hilo en un sistema x86_64 lee un valor por primera vez, obtiene una copia segura para hilos. Sus únicos cambios posteriores puede dejar de verse. Este puede no ser el caso en otras CPU.

Si prueba esto, puede ver que cada hilo está atascado con su valor local.

public class RequiresVolatileMain { 
    static volatile boolean value; 

    public static void main(String... args) { 
     new Thread(new MyRunnable(true), "Sets true").start(); 
     new Thread(new MyRunnable(false), "Sets false").start(); 
    } 

    private static class MyRunnable implements Runnable { 
     private final boolean target; 

     private MyRunnable(boolean target) { 
      this.target = target; 
     } 

     @Override 
     public void run() { 
      int count = 0; 
      boolean logged = false; 
      while (true) { 
       if (value != target) { 
        value = target; 
        count = 0; 
        if (!logged) 
         System.out.println(Thread.currentThread().getName() + ": reset value=" + value); 
       } else if (++count % 1000000000 == 0) { 
        System.out.println(Thread.currentThread().getName() + ": value=" + value + " target=" + target); 
        logged = true; 
       } 
      } 
     } 
    } 
} 

imprime lo siguiente que muestra su desplazamiento del valor, pero se atasca.

Sets true: reset value=true 
Sets false: reset value=false 
... 
Sets true: reset value=true 
Sets false: reset value=false 
Sets true: value=false target=true 
Sets false: value=true target=false 
.... 
Sets true: value=false target=true 
Sets false: value=true target=false 

Si añado -XX:+PrintCompilation este interruptor pasa por el tiempo que se ve

1705 1 % RequiresVolatileMain$MyRunnable::run @ -2 (129 bytes) made not entrant 
1705 2 % RequiresVolatileMain$MyRunnable::run @ 4 (129 bytes) 

Lo que sugiere el código ha sido compilado a nativa es una manera que no se hilo de seguridad.

si se hace el valor volatile lo ves voltear el valor sin fin (o hasta que me aburrí)

EDIT: Lo que esto hace es prueba; cuando detecta que el valor no es el valor objetivo del subproceso, establece el valor. es decir. thread 0 establece a true y thread 1 sets a false Cuando los dos hilos están compartiendo el campo correctamente, se ven cambios entre sí y el valor cambia constantemente entre verdadero y falso.

Sin volátil, esto falla y cada subproceso solo ve su propio valor, por lo que ambos cambian el valor y el subproceso 0 vea true y el subproceso 1 ve false para el mismo campo.

+1

¿Podría proporcionar un ejemplo con un ciclo que demuestre el problema? –

+0

¡No estoy seguro de obtener esto! –

+0

He agregado una explicación. Avísame si tienes más dudas/preguntas. –

1

Dependiendo de su sistema operativo, Thread.yield() podría funcionar o no. Thread.yield() realmente no se puede considerar plataforma independiente, y no se debe usar si necesita esa suposición.

Haciendo el ejemplo de hacer lo que espera que haga, creo que es más una cuestión de arquitectura de procesador que otra cosa ... intente ejecutarlo en diferentes máquinas, con diferentes sistemas operativos, vea lo que puede obtener de él.

+0

Esto tiene poco que ver con thread.yield ... se trata del modelo de memoria compartida en java. –

2

No es 100% seguro de esto, pero podría estar relacionado this:

qué se entiende por reordenamiento?

Hay una serie de casos en los que accede a las variables (campos de instancia de objeto, los campos estáticos de clase, y elementos de la matriz) de programa pueden aparecerá para ejecutar en un orden diferente que fue especificado por el programa . El compilador puede tomar libertades con el orden de las instrucciones en nombre de la optimización. Los procesadores pueden ejecutar instrucciones fuera de servicio bajo ciertas circunstancias. Los datos pueden ser movidos entre registros, cachés de procesador y memoria principal en orden diferente al especificado por el programa.

Por ejemplo, si un hilo se escribe en el campo una y luego a campo B, y el valor de b no depende del valor de a, entonces el compilador es libre para reordenar estas operaciones, y la memoria caché es libre para descargar b a la memoria principal antes de a. Hay varias fuentes potenciales de reordenamiento , como el compilador, el JIT y el caché.

El compilador, tiempo de ejecución, y el hardware se supone que conspiran para crear la ilusión de que, si en serie semántica, lo que significa que en un programa de un solo subproceso , el programa no debería ser capaz de observar los efectos de reordenamientos. Sin embargo, reordenamientos pueden entrar en juego en programas multihilo incorrectamente sincronizados, donde un hilo es capaz de observar los efectos de otros hilos, y pueden ser capaces de detectar esa variable accesos se hacen visibles a otros hilos en un orden diferente de ejecutado o especificado en el programa.

+0

+1 La impresión de 0 se debe a un reordenamiento. Del libro en sí: "NoVisibility podría imprimir cero porque la escritura a listo podría hacerse visible para el hilo del lector antes del número de escritura, un fenómeno conocido como reordenamiento" – zengr

6

El modelo de memoria Java define lo que se requiere para trabajar y lo que no. la "belleza" del código inseguro multihilo es que en la mayoría de las situaciones (especialmente en entornos de desarrollo controlados) generalmente funciona.es solo cuando se llega a la producción con una mejor computadora y la carga aumenta y el JIT realmente se activa cuando los errores comienzan a morder.

+0

que no creo que sea una respuesta. – zengr

+1

¡no es una respuesta segura sino una cruda realidad! +1 – mawia

2

Creo que el punto principal acerca de esto es que no se garantiza que todos los jvms reordenarán las instrucciones de la misma manera. Se utiliza como ejemplo que existen diferentes reordenamientos posibles y, por lo tanto, para algunas implementaciones de jvm puede obtener resultados diferentes. Da la casualidad de que usted jvm está reordenando de la misma manera todo el tiempo, pero ese podría no ser el caso de otro. La única forma de garantizar el orden es usar una sincronización adecuada.

0

Consulte el siguiente código, que presenta el error de visibilidad de datos en x86. intentado con jdk8 y JDK7

package com.snippets; 


public class SharedVariable { 

    private static int sharedVariable = 0;// declare as volatile to make it work 
    public static void main(String[] args) throws InterruptedException { 

     new Thread(new Runnable() { 

      @Override 
      public void run() { 
       try { 
        Thread.sleep(1000); 
       } catch (InterruptedException e) { 
        e.printStackTrace(); 
       } 
       sharedVariable = 1; 
      } 
     }).start(); 

     for(int i=0;i<1000;i++) { 
      for(;;) { 
       if(sharedVariable == 1) { 
        break; 
       } 
      } 
     } 
     System.out.println("Value of SharedVariable : " + sharedVariable); 
    } 

} 

truco no es de esperar que el procesador para hacer el reordenamiento en lugar hacer compilador para hacer un poco de optimización que introduce el error de visibilidad.

Si ejecuta el código anterior, verá que se cuelga indefinidamente porque nunca ve el valor actualizado sharedVariable.

Para corregir el código declara sharedVariable como volátil.

¿Por qué la variable normal no funcionó y el programa anterior se bloquea?

  1. sharedVariable no fue declarado como volátil.
  2. Ahora, porque sharedVariable no se declaró como compilador volátil optimiza el código. Ve que sharedVariable no va a cambiar, así que ¿por qué debería leer de memoria cada vez en el ciclo? Hará que la variable compartida salga del ciclo. Algo similar a abajo.

f

for(int i=0;i<1000;i++)/**compiler reorders sharedVariable 
as it is not declared as volatile 
and takes out the if condition out of the loop 
which is valid as compiler figures out that it not gonna 
change sharedVariable is not going change **/ 
    if(sharedVariable != 1) { 
    for(;;) {} 
    }  
} 

compartido en github: https://github.com/lazysun/concurrency/blob/master/Concurrency/src/com/snippets/SharedVariable.java

Cuestiones relacionadas