2010-03-09 31 views
11

¿Cómo puedo demostrar rápidamente que la siguiente clase no es segura para subprocesos (ya que usa la inicialización diferida y no usa sincronización) escribiendo algún código? En otras palabras, si estoy probando la siguiente clase para seguridad de hilos, ¿cómo puedo fallar?Comprobando que el siguiente código no es seguro para hilos

public class LazyInitRace { 
    private ExpensiveObject instance = null; 

    public ExpensiveObject getInstance() { 
    if (instance == null) 
     instance = new ExpensiveObject(); 
    return instance; 
    } 
} 
+0

¿Agregaría un 'Thread.sleep' en la ayuda del constructor? – Amarghosh

+2

¿* Tiene * para probarlo usando el código, o puede probarlo de otras maneras? Un diagrama de ejecución simple podría usarse para demostrar que no es seguro. – Herms

+0

¿Qué hay para demostrar? Esto no es seguro para subprocesos. – ChaosPandion

Respuesta

1

Bien ... El resultado de este código será falso, donde se espera que sea cierto.

import java.util.concurrent.Callable; 
import java.util.concurrent.ExecutionException; 
import java.util.concurrent.FutureTask; 

public class LazyInitRace { 

    public class ExpensiveObject { 
     public ExpensiveObject() { 
      try { 
       Thread.sleep(1000); 
      } catch (InterruptedException e) { 
      } 
     } 
    } 

    private ExpensiveObject instance = null; 

    public ExpensiveObject getInstance() { 
     if (instance == null) 
      instance = new ExpensiveObject(); 
     return instance; 
    } 

    public static void main(String[] args) { 
     final LazyInitRace lazyInitRace = new LazyInitRace(); 

     FutureTask<ExpensiveObject> target1 = new FutureTask<ExpensiveObject>(
       new Callable<ExpensiveObject>() { 

        @Override 
        public ExpensiveObject call() throws Exception { 
         return lazyInitRace.getInstance(); 
        } 
       }); 
     new Thread(target1).start(); 

     FutureTask<ExpensiveObject> target2 = new FutureTask<ExpensiveObject>(
       new Callable<ExpensiveObject>() { 

        @Override 
        public ExpensiveObject call() throws Exception { 
         return lazyInitRace.getInstance(); 
        } 
       }); 
     new Thread(target2).start(); 

     try { 
      System.out.println(target1.get() == target2.get()); 
     } catch (InterruptedException e) { 
     } catch (ExecutionException e) { 
     } 
    } 
} 
+0

Si omito el Thread.sleep() en el constructor, entonces devuelve verdadero. Tal vez si lo ejecuto miles de veces, a veces me mostraré falso. – portoalet

12

¿Puede forzar que ExpensiveObject tarden en construirse dentro de su prueba? Si es así, simplemente llame al getInstance() dos veces desde dos subprocesos diferentes, en un tiempo suficientemente corto para que el primer constructor no se haya completado antes de que se realice la segunda llamada. Al final se construirán dos instancias diferentes, que es donde debe fallar.

Hacer un doble bloqueo de verificación inocente será más difícil, tenga en cuenta ... (aunque no es seguro sin especificar volatile para la variable).

+1

@Jon Skeet: Disculpe mi ignorancia, pero me gustaría saber el significado específico de volátil. –

+1

volátil es una palabra clave java utilizada para controlar cómo el jvm maneja el acceso a la memoria entre los hilos. –

-1

Bueno, no es seguro para subprocesos. Verificación del hilo de seguridad es al azar, sino más bien sencilla:

  1. Hacer ExpensiveObject constructor totalmente segura:

    ExpensiveObject sincronizado() {...

  2. lugar para constructor de código que comprueba si otra copia de objeto existe - luego elevar una excepción.

  3. Crear hilo método seguro para borrar 'ejemplo' variable

  4. Place código secuencial de getInstance/clearInstance a bucle para su ejecución por múltiples hilos y esperar excepción de (2)

+0

No creo que estuviera pidiendo métodos para * hacer * seguro el hilo del código, sino una manera de * mostrar * que no es. –

+0

@Michael Borgwardt mira (2) - el aumento de la excepción es MUESTRA de inseguridad. Todos los otros trucos sincronizados que he usado solo para evitar la introducción de un nuevo problema en el código ya existente. – Dewfy

+0

Su número 1 no es válido, no se compilará. "sincronizado" no es un modificador válido para un constructor. Todos los constructores son intrínsecamente seguros para subprocesos. Además de eso, la solución propuesta es demasiado compleja ... simplemente sincronice el método estático "getInstance". – NateS

14

Por definición , las condiciones de carrera no se pueden probar de manera determinista, a menos que controle el programador de hilos (que no lo hace). Lo más cercano que puede hacer es agregar un retraso configurable en el método getInstance(), o escribir el código donde el problema podría manifestar y ejecutarlo miles de veces en un bucle.

Por cierto, nada de esto realmente constituye "prueba". Formal Verification, pero es muy, muy difícil de hacer, incluso para cantidades relativamente pequeñas de código.

+5

+1 para el uso correcto de la palabra "prueba" – Seth

+3

Cualquier demostración de que falla * demuestra * que puede fallar, por lo que realmente no veo ningún problema con el uso de "prueba" aquí. – Herms

+2

de acuerdo. prueba por contraejemplo es una prueba válida. –

0

Deja un cálculo muy largo en el constructor:

public ExpensiveObject() 
{ 
    for(double i = 0.0; i < Double.MAX_VALUE; ++i) 
    { 
     Math.pow(2.0,i); 
    } 
} 

Es posible que desee para disminuir la condición de terminación de Double.MAX_VALUE/2.0 o dividir por un número mayor si MAX_VALUE está tomando demasiado tiempo para su gusto.

3

Dado que se trata de Java, puede utilizar la biblioteca thread-weaver para inyectar pausas o descansos en su código y controlar múltiples hilos de ejecución. De esta forma, puede obtener un constructor ExpensiveObject lento sin tener que modificar el código del constructor, como otros sugirieron (correctamente).

+0

Nunca escuché hablar de esa biblioteca antes. Parece bastante interesante. – Herms

5

Esto no está utilizando el código, pero aquí hay un ejemplo de cómo lo probaría.Olvidé el formato estándar para diagramas de ejecución como este, pero el significado debería ser lo suficientemente obvio.

| Thread 1    | Thread 2    | 
|-----------------------|-----------------------| 
| **start**    |      | 
| getInstance()   |      | 
| if(instance == null) |      | 
| new ExpensiveObject() |      | 
| **context switch ->** | **start**    | 
|      | getInstance()   | 
|      | if(instance == null) | //instance hasn't been assigned, so this check doesn't do what you want 
|      | new ExpensiveObject() | 
| **start**    | **<- context switch** | 
| instance = result  |      | 
| **context switch ->** | **start**    | 
|      | instance = result  | 
|      | return instance  | 
| **start**    | **<- context switch** | 
| return instance  |      | 
0

Puede probarlo fácilmente con el depurador.

  1. escribir un programa que llama getInstance() en dos hilos separados.
  2. Establezca un punto de interrupción en la construcción del ExpensiveObject. Asegúrese de que el depurador solo suspenderá el subproceso , no la máquina virtual.
  3. Luego, cuando el primer hilo se detiene en el punto de interrupción, déjelo suspendido.
  4. Cuando se detiene el segundo hilo, simplemente continúa.
  5. Si marca el resultado de la llamada getInstance() para ambos hilos, se referirán a diferentes instancias .

La ventaja de hacerlo de esta manera, es que en realidad no necesita un objeto caro, cualquier objeto producirá los mismos resultados. Simplemente está utilizando el depurador para programar la ejecución de esa línea de código particular y así crear un resultado determinista.

Cuestiones relacionadas