2012-05-21 5 views
9

Imagine que tiene un patrón típico productor-consumidor en Java. Para ser un poco más eficiente, quiere usar notify() y no notifyAll() cuando se agrega un nuevo elemento a la cola. Si dos subprocesos de productor invocan notificar, ¿está garantizado que se despierten dos subprocesos distintos de consumidor en espera? ¿O puede ser que dos notify() s disparados poco después causen que el mismo hilo del cliente se ponga en cola para despertarse dos veces? No puedo encontrar la sección que es la API que describe cómo funciona esto exactamente. ¿Java tiene alguna operación interna atómica para despertar hilos exactamente una vez?¿Se puede avisar si se activa el mismo hilo varias veces?

Si solo un usuario espera, se perderá la segunda notificación, eso no es problema.

+4

¿Por qué no solo utiliza una implementación de BlockingQueue? –

+0

Estoy de acuerdo con Mauricio. Manténgase alejado de wait()/notify() si es posible - es un nido de abejas de condiciones de carrera, API mal documentadas y "wake-ups espurios". –

Respuesta

13

Mi respuesta tiene cierta información específica de la implementación. Se basa en mi conocimiento práctico de Sun JVM y otros comportamientos de biblioteca de subprocesos.

Si dos subprocesos del productor invocan notificar, ¿se garantiza que se despierten dos subprocesos de consumidor distintos en espera?

No, no lo es. No hay garantía de que habrá consumidores despertados. Lo que está garantizado es que si hay 2 subprocesos en espera, entonces 2 diferentes subprocesos se colocarán en la cola de ejecución.

O puede ser que dos notify() s despedido poco después de la otra causa el mismo hilo comsumer a poner en cola para despertar dos veces?

No. Two notify() llamadas no hará que el mismo hilo de consumidor se ponga en cola dos veces. Sin embargo, puede provocar que se despierte un subproceso y que no haya otros subprocesos esperando, por lo que la segunda llamada notify() puede no hacer nada. Por supuesto, el hilo podría haberse despertado y luego volverse a esperar otra vez, así que obtenga la segunda llamada notify() de esa manera, pero no creo que eso sea lo que está preguntando.

¿Tiene java algo de funcionamiento interno atómico para despertar los hilos exactamente una vez?

Sí. El código Thread tiene una cantidad de puntos de sincronización. Una vez que un hilo ha sido notificado, se retira de la cola wait. Las futuras llamadas al notify() buscarán en la cola wait y no encontrarán el hilo.

Uno más importante punto. Con los modelos de productor/consumidor, siempre asegúrese de estar probando la condición en un bucle while. La razón es que hay condiciones de carrera con los consumidores que están bloqueados en el candado pero que no esperan la condición.

synchronized (workQueue) { 
    // you must do a while here 
    while (workQueue.isEmpty()) { 
     workQueue.wait(); 
    } 
    workQueue.remove(); 
} 

Consumer1 podría estar esperando en workQueue. Consumer2 podría bloquearse en synchronized pero en la cola de ejecución. Si se pone algo en workQueue y workQueue.notify() se llama. Consumer2 se pone en cola de ejecución ahora pero es detrás deConsumer1 que está allí primero. Esta es una implementación común. Por lo tanto, Consumer1 va a eliminar el artículo del workQueue sobre el que se notificó Consumer2. Consumer2 tiene que volver a probar si el workQueue está vacío; de lo contrario, remove() arrojará porque la cola está vacía de nuevo. Consulte aquí para more details of the race.

También es importante tener en cuenta que los despertadores espurios se han documentado, por lo que el lazo while protege contra un hilo que se despierta sin una llamada wait().

Dicho todo esto, si puede reducir su código de productor/consumidor utilizando un BlockingQueue como se recomienda en otras respuestas, entonces debe hacerlo. El código BlockingQueue ya ha resuelto todos estos problemas.

+0

respuesta perfecta, mi pregunta debe haber sido un poco confusa, pero entendiste lo que quería escuchar :-) –

2

Desde el javadoc for notify():

La elección del hilo para notificar "es arbitraria y se produce según el criterio de la aplicación"

es casi seguro que no sea un "justo" (como el término es utilizado en informática y paralelismo) algoritmo para despertar hilos. Es muy posible que el mismo hilo se despierte dos veces en rápida sucesión. También tenga en cuenta que las notificaciones espurias también son posibles.

En general, estoy de acuerdo con los comentarios que sugieren el uso de una implementación BlockingQueue en lugar de hacerlo usted mismo.

3

Sí, lo que usted describe puede suceder.

Como se describe en javadoc, notify se activa un hilo arbitrario. Por lo tanto, si el hilo ha terminado y ha llamado al wait antes del siguiente notify, entonces es uno de los candidatos arbitrarios para despertar.

Tengo amplia experiencia con las aplicaciones multitarea, y me parece que yo uso uno de estos dos patrones:

  1. Hay varios subprocesos para dormir que es necesario despertar en un evento, y el orden en que ellos despiertan no importa. En este caso, uso notifyAll para activarlos.

  2. Hay un hilo de dormir que debe activarse en un evento. En este caso, uso notify para activarlo.

Si alguna vez tengo una situación en la que hay múltiples hilos de sueño y quiero sólo para despertar a uno de ellos, que utilice un diseño diferente para lograr esto. Básicamente, construyo algo por mí mismo para que el entorno de ejecución no tome ninguna decisión arbitraria. Siempre quiero saber exactamente qué despertará.

Descompongo un diseño en uno de estos dos escenarios, o utilizo algo del paquete java.util.concurrent. Nunca tengo problemas con las notificaciones espurias, pero también soy muy cuidadoso con los objetos que uso para bloquear. Tiendo a crear instancias vanil Object cuyo único propósito es ser el objetivo de una operación de bloqueo, pero ocasionalmente usaré un objeto cuyo tipo de clase está bien definido y bajo mi control.

2

Puede usar ReentrantLock para obtener una política de pedido de llegada justa. La interfaz ReadWriteLock, obtiene el comportamiento productor-consumidor. La clase ReentrantReadWriteLock combina ambas capacidades.

O

Debe utilizar ArrayBlockingQueue, donde este patrón ya está implementado.

int capacity = 10; 
boolean fair = true; 
new ArrayBlockingQueue(capacity, fair); 
Cuestiones relacionadas