2010-07-30 15 views
6

Específicamente, ¿alguien puede decirme qué está mal con este fragmento de código? Debería iniciar los hilos, por lo que debería imprimir "Ingresando hilo ..." 5 veces y luego esperar hasta que se llame a notifyAll(). Pero, al azar, imprime "Entrando ..." y "Hecho ..." y sigue esperando a los demás.Cómo usar el protocolo de esperar y notificar con varios hilos

public class ThreadTest implements Runnable { 
    private int num; 
    private static Object obj = new Object(); 
    ThreadTest(int n) { 
     num=n; 
    } 
    @Override 
    public void run() { 
     synchronized (obj) { 
      try { 
       System.out.println("Entering thread "+num); 
       obj.wait(); 
       System.out.println("Done Thread "+num); 
      } catch (InterruptedException e) { 
       e.printStackTrace(); 
      } 
     } 
    } 

    public static void main(String[] args) { 
     Runnable tc; 
     Thread t; 
     for(int i=0;i<5;i++) { 
      tc = new ThreadTest(i); 
      t = new Thread(tc); 
      t.start(); 
     } 
     synchronized (obj) { 
      obj.notifyAll(); 
     } 
    } 
} 

Respuesta

8

Usted no está haciendo nada descaradamente mal con las llamadas de método, pero sí tiene un race condition.

pesar de que en un mundo ideal el hilo principal alcanzará su bloque sincronizado después de que todos los subprocesos de trabajo llegan a la llamada de espera(), no hay garantía de que (se dice explícitamente que la máquina virtual que usted no quería los hilos para ejecutar en secuencia con el hilo principal haciéndolos hilos). Puede suceder (por ejemplo, si tiene solo un núcleo) que el planificador de hilos decide bloquear todos los hilos de trabajo de inmediato para permitir que el hilo principal continúe. Puede ser que los subprocesos de trabajo se cambien de contexto debido a una falta de memoria caché. Puede ser que un subproceso de trabajador bloquee E/S (la instrucción de impresión) y el hilo principal se conecte en su lugar.

Por lo tanto, si el subproceso principal logra alcanzar el bloque sincronizado antes de que todos los subprocesos de trabajo hayan alcanzado la llamada wait(), los subprocesos de trabajo que no hayan alcanzado la llamada wait() no funcionarán como se esperaba. Como la configuración actual no le permite controlar esto, debe agregar un manejo explícito de esto. Puede agregar algún tipo de variable que se incremente a medida que cada subproceso llegue a wait() y hacer que el subproceso principal no llame a notifyAll() hasta que esta variable llegue a 5, o puede tener el bucle principal y llamar de manera repetida a notifyAll(), para que los hilos de trabajo se liberen en múltiples grupos.

Eche un vistazo al paquete java.util.concurrent - hay varias clases de bloqueo que proporcionan capacidades más potentes que las cerraduras sincronizadas básicas - como siempre, Java le evita reinventar la rueda. CountDownLatch parece ser particularmente relevante.

En resumen, la concurrencia es difícil. Debe diseñar para asegurarse de que todo funciona aún cuando los subprocesos se ejecutan en los pedidos que no desea, así como los pedidos que desea.

+0

Gracias Scott por la respuesta. Supongo que una solución simple aquí sería poner Thread.sleep (1000) justo antes de notifyAll() para asegurarse de que todos los hilos hayan entrado en su estado de espera() – figaro

+0

Eso bien podría funcionar, pero es una solución ingenua. Si desea hacer más trabajo antes de la llamada de espera, si es necesario que los hilos de trabajo se liberen lo antes posible o incluso si los hilos de trabajo solo tardan más de lo que había estimado, se encontrará con el mismo problema. Usted * debe * usar el bloqueo. De eso se trata la programación simultánea. Eche un vistazo al código de David Blevin: es un excelente ejemplo de cómo usar los bloqueos de cuenta atrás. – Scott

0

La raíz del problema con su código es que algunos de los hilos no llegan a la wait hasta después el hilo principal llama notifyAll. Entonces cuando esperan, nada los despertará.

Para que esto funcione (usando wait/notify) necesita sincronizar el hilo principal para que espere a que todos los hilos secundarios lleguen a un estado donde puedan recibir la notificación antes de realizar esa llamada.

En general, hacer la sincronización con wait, notify y bloqueos primitivos es complicado. En la mayoría de los casos, obtendrá mejores resultados (es decir, un código más simple, más confiable y más eficiente) utilizando las clases de utilidad de simultaneidad de Java.

2

En segundo lugar la recomendación CountDownLatch. Aquí está el patrón que utilizo para mis pruebas multiproceso:

final int threadCount = 200; 

final CountDownLatch startPistol = new CountDownLatch(1); 
final CountDownLatch startingLine = new CountDownLatch(threadCount); 
final CountDownLatch finishingLine = new CountDownLatch(threadCount); 

// Do a business method... 
Runnable r = new Runnable() { 
    public void run() { 
     startingLine.countDown(); 
     try { 
      startPistol.await(); 

      // TODO: challenge the multithreadedness here 

     } catch (InterruptedException e) { 
      Thread.interrupted(); 
     } 
     finishingLine.countDown(); 
    } 
}; 

// -- READY -- 

for (int i = 0; i < threadCount; i++) { 
    Thread t = new Thread(r); 
    t.start(); 
} 

// Wait for the beans to reach the finish line 
startingLine.await(1000, TimeUnit.MILLISECONDS); 

// -- SET -- 

// TODO Assert no one has started yet 

// -- GO -- 

startPistol.countDown(); // go 

assertTrue(finishingLine.await(5000, TimeUnit.MILLISECONDS)); 

// -- DONE -- 

// TODO: final assert 

La idea es garantizar la siguiente línea de código sus hilos se ejecutará es el que se opone a la multithreadedness o tan cerca de ella como sea posible. Pienso en los hilos como corredores en una pista.Los pones en la pista (crear/iniciar), esperas que se alineen perfectamente en la línea de partida (todos han llamado a startingLine.countDown()), luego disparas la pistola de inicio (startPistol.countDown()) y esperas a todos para cruzar la línea de meta (finishingLine.countDown()).

[EDITAR] Se debe tener en cuenta que si no tiene ningún código o comprobaciones que desee ejecutar entre startingLine.await() y startingPistol.countDown(), puede combinar tanto startingLine como startingPistol en un CyclicBarrier (threadCount + 1). El enfoque doble CountDownLatch es efectivamente el mismo y permite que el hilo de prueba principal realice cualquier configuración/comprobación después de que todos los demás hilos estén alineados y antes de que comiencen a ejecutarse, si fuera necesario.

Cuestiones relacionadas