2012-05-21 20 views
9

Immagina di avere un tipico modello produttore-consumatore in Java. Per essere un po 'più efficiente, si desidera utilizzare notify() e non notifyAll() quando un nuovo elemento viene aggiunto alla coda. Se due thread di produzione invocano la notifica, è garantito che due thread di attesa in attesa distinti verranno risvegliati? Oppure è possibile che due sottrazioni notify() siano attivate l'una dopo l'altra causando l'accodamento dello stesso thread del computer per due volte? Non riesco a trovare la sezione è l'API che descrive come funziona esattamente. Java ha alcune operazioni interne atomiche per risvegliare i thread esattamente una volta?È possibile avvisare di attivare lo stesso thread più volte?

Se solo un utente è in attesa, la seconda notifica verrà persa, non c'è problema.

+4

Perché non usi semplicemente un'implementazione di BlockingQueue? –

+0

Sono d'accordo con Mauricio. Evita l'attesa()/notify() se possibile: è un vespaio di condizioni di gara, API scarsamente documentate e "wakeup spurie". –

risposta

13

La mia risposta ha alcune informazioni specifiche sull'implementazione. Si basa sulla mia conoscenza pratica di Sun JVM e di altri comportamenti delle librerie di thread.

Se due thread di produttore richiamano la notifica, è garantito che due thread di attesa distinti verranno risvegliati?

No, non lo è. Non c'è alcuna garanzia che ci saranno dei consumatori risvegliati. Ciò che è garantito è che se ci sono 2 thread in attesa, quindi 2 diversi thread verranno messi nella coda di esecuzione.

oppure può essere che due notify() s sparato poco dopo l'altra causa lo stesso thread comsumer da coda per sveglia due volte?

No. Due chiamate notify() non provocano lo stesso thread di consumo in coda due volte. Potrebbe tuttavia causare il risveglio di un thread e l'eventuale attesa di altri thread, pertanto la seconda chiamata notify() potrebbe non fare nulla. Ovviamente il thread avrebbe potuto essere svegliato e poi sarebbe tornato indietro aspettando di nuovo e quindi ottenere la seconda chiamata notify() in quel modo, ma non penso che sia quello che stai chiedendo.

java ha qualche operazione interna atomica per risvegliare i fili esattamente una volta?

Sì. Il codice Thread ha un numero di punti di sincronizzazione. Una volta che un thread è stato notificato, viene spostato fuori dalla coda wait. Le future chiamate a notify() esamineranno la coda wait e non troveranno il thread.

Un altro importante punto. Con i modelli produttore/consumatore, assicurati sempre di testare la condizione in un ciclo while. Il motivo è che ci sono condizioni di gara con i consumatori che sono bloccati sulla serratura ma non aspettano le condizioni.

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

Consumer1 potrebbe essere in attesa su workQueue. Consumer2 potrebbe essere bloccato su synchronized ma nella coda di esecuzione. Se qualcosa viene inserito in workQueue e workQueue.notify() viene chiamato. Consumer2 è ora messo in coda di esecuzione ma è dietroConsumer1 chi è il primo. Questa è un'implementazione comune. Quindi Consumer1 inserisce un elemento rimosso da workQueue a cui è stato notificato Consumer2. Consumer2 deve eseguire nuovamente il test se workQueue è vuoto altrimenti verrà generato remove() perché la coda è di nuovo vuota. Vedi qui per more details of the race.

È anche importante rendersi conto che le scorte spurie sono state documentate in modo che il ciclo while protegga contro un thread che si sta svegliando senza una chiamata wait().

Tutto ciò detto, se è possibile ridurre il codice produttore/consumatore utilizzando un BlockingQueue come consigliato in altre risposte, è necessario farlo. Il codice BlockingQueue ha già risolto tutti questi problemi.

+0

risposta perfetta, la mia domanda deve essere stata un po 'oscura, ma hai capito cosa volevo sentire :-) –

2

Dal javadoc for notify():

La scelta di quale thread di notifica "è arbitraria e si verifica a discrezione della realizzazione"

sarà quasi certamente non essere un "equo" (come il termine è usato in informatica e parallelismo) algoritmo per risvegliare i thread. È del tutto possibile che lo stesso thread venga svegliato due volte in rapida successione. Notare anche che sono possibili notifiche spurie.

In generale, sono d'accordo con i commenti che suggeriscono di utilizzare un'implementazione BlockingQueue invece di farlo da soli.

3

Sì, ciò che descrivi può accadere.

Come descritto nello javadoc, notify riattiva un thread arbitrario. Quindi, se il tuo thread è finito e ha chiamato wait prima del prossimo notify, allora è uno dei candidati arbitrari per la veglia.

Ho una vasta esperienza con le applicazioni multithread, e trovo che uso uno di questi due modelli:

  1. Non ci sono più thread di sonno che hanno bisogno di svegliarsi su un evento, e l'ordine in cui si svegliano non importa. In questo caso, utilizzo notifyAll per riattivarli.

  2. C'è un thread in sospensione che deve essere attivato su un evento. In questo caso, io uso notify per svegliarlo.

Se mai dovessi avere una situazione in cui ci sono più thread di sonno e voglio svegliarmi solo uno di loro, utilizzare un design diverso per raggiungere questo obiettivo. Fondamentalmente, costruisco qualcosa fuori me stesso in modo che non ci sia alcuna decisione arbitraria da parte dell'ambiente di runtime. Voglio sempre sapere esattamente cosa si sveglierà.

Analizzo un disegno in uno di questi due scenari oppure utilizzo qualcosa dal pacchetto java.util.concurrent. Non ho mai problemi con le notifiche spurie, ma sono anche molto attento agli oggetti che uso per il blocco. Tendo a creare istanze di vaniglia Object il cui unico scopo è quello di essere l'obiettivo di un'operazione di blocco, ma occasionalmente userò un oggetto il cui tipo di classe è ben definito e sotto il mio controllo.

2

È possibile utilizzare ReentrantLock per ottenere una politica di ordine di arrivo equo. L'interfaccia ReadWriteLock, si ottiene il comportamento produttore-consumatore. La classe ReentrantReadWriteLock combina entrambe le funzionalità.

O

Si dovrebbe usare ArrayBlockingQueue, dove questo modello è già in atto.

int capacity = 10; 
boolean fair = true; 
new ArrayBlockingQueue(capacity, fair); 
Problemi correlati