Perché deve aspettare () essere sempre nel blocco sincronizzato


257

Sappiamo tutti che per invocare Object.wait()questa chiamata deve essere inserita in un blocco sincronizzato, altrimenti IllegalMonitorStateExceptionviene lanciata. Ma qual è la ragione per fare questa restrizione? So che wait()rilascia il monitor, ma perché dobbiamo acquisire esplicitamente il monitor rendendo sincronizzato un determinato blocco e quindi rilasciare il monitor chiamandowait() ?

Qual è il danno potenziale se fosse possibile invocare wait()al di fuori di un blocco sincronizzato, conservandone la semantica - sospendere il thread del chiamante?

Risposte:


232

A wait()ha senso solo quando c'è anche un notify(), quindi si tratta sempre di comunicazione tra thread e che ha bisogno della sincronizzazione per funzionare correttamente. Si potrebbe sostenere che questo dovrebbe essere implicito, ma che non sarebbe davvero di aiuto, per il seguente motivo:

Semanticamente, non lo hai mai fatto wait(). Hai bisogno di alcune condizioni per essere satsificato, e se non lo è, aspetti fino a quando non lo è. Quindi quello che fai davvero è

if(!condition){
    wait();
}

Ma la condizione viene impostata da un thread separato, quindi per far funzionare correttamente questa operazione è necessaria la sincronizzazione.

Un altro paio di cose sbagliate, solo perché il tuo thread ha smesso di aspettare non significa che la condizione che stai cercando è vera:

  • È possibile ottenere risvegli spuri (il che significa che un thread può svegliarsi dall'attesa senza aver mai ricevuto una notifica), oppure

  • La condizione può essere impostata, ma un terzo thread rende nuovamente la condizione falsa quando il thread in attesa si riattiva (e riacquista il monitor).

Per far fronte a questi casi ciò di cui hai veramente bisogno è sempre una variazione di questo:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

Meglio ancora, non scherzare affatto con le primitive di sincronizzazione e lavorare con le astrazioni offerte nei java.util.concurrentpacchetti.


3
C'è anche una discussione dettagliata qui, che dice essenzialmente la stessa cosa. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…

1
tra l'altro, se non si deve ignorare la bandiera interrotta, anche il loop deve controllare Thread.interrupted().
bestsss

2
Posso ancora fare qualcosa del tipo: while (! Condition) {synchronized (this) {wait ();}} il che significa che c'è ancora una corsa tra il controllo della condizione e l'attesa anche se wait () è correttamente chiamato in un blocco sincronizzato. Quindi c'è qualche altro motivo dietro questa restrizione, forse a causa del modo in cui è implementato in Java?
shrini1000,

9
Un altro brutto scenario: la condizione è falsa, stiamo per entrare in wait () e poi un altro thread cambia la condizione e invoca notification (). Poiché non siamo ancora in wait (), ci mancherà questa notifica (). In altre parole, testare e attendere, nonché cambiare e notificare deve essere atomico .

1
@Nullpointer: se si tratta di un tipo che può essere scritto atomicamente (come il booleano implicito utilizzandolo direttamente in una clausola if) e non vi è alcuna interdipendenza con altri dati condivisi, è possibile cavarsela dichiarandola volatile. Ma hai bisogno di quello o della sincronizzazione per assicurarti che l'aggiornamento sia visibile prontamente agli altri thread.
Michael Borgwardt,

283

Qual è il danno potenziale se fosse possibile invocare wait()al di fuori di un blocco sincronizzato, conservandone la semantica - sospendere il thread del chiamante?

Illustriamo quali problemi incontreremmo se wait()potessimo essere chiamati al di fuori di un blocco sincronizzato con un esempio concreto .

Supponiamo che dovessimo implementare una coda di blocco (lo so, ce n'è già una nell'API :)

Un primo tentativo (senza sincronizzazione) potrebbe guardare qualcosa lungo le linee sottostanti

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

Questo è ciò che potrebbe potenzialmente accadere:

  1. Un thread consumer chiama take()e vede che il buffer.isEmpty().

  2. Prima che il thread del consumatore continui a chiamare wait(), arriva un thread del produttore che invoca un full give(), ovverobuffer.add(data); notify();

  3. Il filo consumatore ora chiamare wait()(e perdere il notify()che è stato appena chiamato).

  4. Se sfortunato, il thread del produttore non produrrà di più give()a causa del fatto che il thread del consumatore non si sveglia mai e abbiamo un dead-lock.

Una volta compreso il problema, la soluzione è ovvia: utilizzare synchronizedper assicurarsi che notifynon venga mai chiamato tra isEmptyewait .

Senza entrare nei dettagli: questo problema di sincronizzazione è universale. Come sottolinea Michael Borgwardt, attendere / notifica riguarda la comunicazione tra thread, quindi si finirà sempre con una condizione di gara simile a quella sopra descritta. Questo è il motivo per cui viene applicata la regola "aspetta solo dentro sincronizzato".


Un paragrafo del link pubblicato da @Willie lo riassume abbastanza bene:

È necessaria la garanzia assoluta che il cameriere e il notificatore concordino sullo stato del predicato. Il cameriere controlla lo stato del predicato ad un certo punto leggermente PRIMA di andare a dormire, ma dipende per correttezza dal fatto che il predicato è vero QUANDO va a dormire. C'è un periodo di vulnerabilità tra questi due eventi, che può interrompere il programma.

Il predicato che il produttore e il consumatore devono concordare è nell'esempio sopra buffer.isEmpty(). E l'accordo viene risolto assicurando che l'attesa e la notifica vengano eseguite in synchronizedblocchi.


Questo post è stato riscritto come articolo qui: Java: Perché aspettare deve essere chiamato in un blocco sincronizzato


Inoltre, per accertarmi che le modifiche apportate alla condizione vengano visualizzate immediatamente dopo la fine di wait (), suppongo. In caso contrario, anche un dead-lock dal momento che è stato già chiamato la notifica ().
Surya Wijaya Madjid,

Interessante, ma nota che solo la chiamata sincronizzata in realtà non risolverà sempre tali problemi a causa della natura "inaffidabile" di wait () e notification (). Maggiori informazioni qui: stackoverflow.com/questions/21439355/… . Il motivo per cui è necessaria la sincronizzazione risiede nell'architettura hardware (vedere la mia risposta di seguito).
Marcus,

ma se aggiungi return buffer.remove();mentre blocchi ma dopo wait();, funziona?
BobJiang

@BobJiang, no, il thread può essere svegliato per motivi diversi da quelli che chiamano dare. In altre parole, il buffer può essere vuoto anche dopo i waitritorni.
aioobe,

Ho solo Thread.currentThread().wait();la mainfunzione circondata da try-catch per InterruptedException. Senza synchronizedblocco, mi dà la stessa eccezione IllegalMonitorStateException. Cosa lo rende raggiungere lo stato illegale ora? Funziona all'interno del synchronizedblocco però.
Shashwat,

12

@Rollerball ha ragione. La wait()si chiama, in modo che il filo può attendere qualche condizione a verificarsi quando questa wait()chiamata accade, il filo è costretto ad abbandonare la sua serratura.
Per rinunciare a qualcosa, devi prima possederlo. Il thread deve prima possedere il blocco. Da qui la necessità di chiamarlo all'interno di un synchronizedmetodo / blocco.

Sì, sono d'accordo con tutte le risposte di cui sopra in merito ai potenziali danni / incongruenze se non hai verificato la condizione all'interno del synchronizedmetodo / blocco. Tuttavia, come ha sottolineato @ shrini1000, solo la chiamata wait()all'interno del blocco sincronizzato non eviterà che questa incoerenza si verifichi.

Ecco una bella lettura ..


5
@Popeye Spiega 'correttamente' correttamente. Il tuo commento non serve a nessuno.
Marchese di Lorne,

4

Il problema può causare se non sincronizzare prima wait()è la seguente:

  1. Se il 1 ° thread entra makeChangeOnX()e controlla la condizione while, ed è true( x.metCondition()restituisce false, significa x.conditionè false) quindi entrerà al suo interno. Quindi, appena prima del wait()metodo, un altro thread passa a setConditionToTrue()e imposta x.conditiona truee notifyAll().
  2. Quindi solo dopo, il 1 ° thread entrerà nel suo wait()metodo (non influenzato da quello notifyAll()che è successo pochi istanti prima). In questo caso, il 1 ° thread rimarrà in attesa dell'esecuzione di un altro thread setConditionToTrue(), ma ciò potrebbe non ripetersi.

Ma se metti synchronizeddavanti ai metodi che cambiano lo stato dell'oggetto, questo non accadrà.

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}

2

Sappiamo tutti che i metodi wait (), notify () e notifyAll () sono utilizzati per le comunicazioni inter-thread. Per sbarazzarsi del segnale perso e dei problemi di risveglio spuri, il thread in attesa attende sempre in alcune condizioni. per esempio-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

Quindi notificando che il set di thread era Variabile notificata su true e notifica.

Ogni thread ha la propria cache locale, quindi tutte le modifiche vengono prima scritte lì e poi promosse gradualmente nella memoria principale.

Se questi metodi non fossero stati richiamati all'interno del blocco sincronizzato, la variabile wasNotified non verrebbe scaricata nella memoria principale e sarebbe presente nella cache locale del thread, in modo che il thread in attesa continuerà ad attendere il segnale sebbene sia stato ripristinato mediante notifica al thread.

Per risolvere questo tipo di problemi, questi metodi vengono sempre richiamati all'interno del blocco sincronizzato, il che assicura che quando si avvia il blocco sincronizzato, tutto verrà letto dalla memoria principale e verrà scaricato nella memoria principale prima di uscire dal blocco sincronizzato.

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

Grazie, spero che chiarisca.


1

Questo in pratica ha a che fare con l'architettura hardware (ovvero RAM e cache ).

Se non lo usi synchronizedinsieme a wait()o notify(), un altro thread potrebbe inserire lo stesso blocco invece di attendere che il monitor lo inserisca. Inoltre, quando ad esempio si accede a un array senza un blocco sincronizzato, un altro thread potrebbe non vedere la modifica ad esso ... in realtà un altro thread non vedrà alcuna modifica ad esso quando ha già una copia dell'array nella cache di livello x ( aka cache di 1 ° / 2 ° / 3 ° livello) del thread che gestisce il core della CPU.

Ma i blocchi sincronizzati sono solo un lato della medaglia: se si accede effettivamente a un oggetto all'interno di un contesto sincronizzato da un contesto non sincronizzato, l'oggetto non verrà ancora sincronizzato nemmeno all'interno di un blocco sincronizzato, poiché contiene una propria copia del oggetto nella sua cache. Ho scritto su questi problemi qui: https://stackoverflow.com/a/21462631 e Quando un blocco contiene un oggetto non finale, il riferimento dell'oggetto può ancora essere modificato da un altro thread?

Inoltre, sono convinto che le cache di livello x siano responsabili della maggior parte degli errori di runtime non riproducibili. Questo perché gli sviluppatori di solito non imparano le cose di basso livello, come il funzionamento della CPU o come la gerarchia della memoria influisce sul funzionamento delle applicazioni: http://en.wikipedia.org/wiki/Memory_hierarchy

Resta un enigma perché le classi di programmazione non iniziano prima con la gerarchia di memoria e l'architettura della CPU. "Ciao mondo" non sarà d'aiuto qui. ;)


1
Ho appena scoperto un sito web che lo spiega perfettamente e in modo approfondito: javamex.com/tutorials/…
Marcus

Mmm .. non sono sicuro di seguirlo. Se la memorizzazione nella cache era l'unico motivo per cui attendere e notifica all'interno sincronizzato, perché la sincronizzazione non viene inserita nell'implementazione di wait / notification?
Aioobe,

Bella domanda, poiché aspettare / notifica potrebbe benissimo essere metodi sincronizzati ... forse gli ex sviluppatori Java di Sun conoscono la risposta? Dai
Marcus,

Un motivo potrebbe essere: nei primi giorni di Java non c'erano errori di compilazione quando non si chiamavano sincronizzati prima di fare queste operazioni di multithreading. Invece c'erano solo errori di runtime (es. Coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Forse hanno davvero pensato a @SUN che quando i programmatori ricevono questi errori vengono contattati, il che potrebbe aver dato loro l'opportunità di vendere più dei loro server. Quando è cambiato? Forse Java 5.0 o 6.0, ma in realtà non ricordo di essere onesto ...
Marcus,

TBH Vedo alcuni problemi con la tua risposta 1) La tua seconda frase non ha senso: non importa su quale oggetto abbia un blocco. Indipendentemente dall'oggetto su cui si sincronizzano due thread, tutte le modifiche vengono rese visibili. 2) Dici che un altro thread "non" vedrà alcun cambiamento. Questo dovrebbe essere "non può" . 3) Non so perché stai aprendo le cache di 1 ° / 2 ° / 3 ° livello ... Ciò che conta qui è ciò che dice il modello di memoria Java e che è specificato in JLS. Mentre l'architettura hardware può aiutare a capire perché JLS dice cosa fa, in questo contesto è strettamente irrilevante.
Aioobe,

0

direttamente da questo tutorial di Oracle Java:

Quando un thread invoca d.wait, deve possedere il blocco intrinseco per d - altrimenti viene generato un errore. Richiamare l'attesa all'interno di un metodo sincronizzato è un modo semplice per acquisire il blocco intrinseco.


Dalla domanda che l'autore ha fatto, non sembra che l'autore della domanda abbia una chiara comprensione di ciò che ho citato dal tutorial. Inoltre, la mia risposta, spiega "Perché".
Rollerball,

0

Quando si chiama notification () da un oggetto t, java notifica un particolare metodo t.wait (). Ma come fa java a cercare e notificare un particolare metodo di attesa.

java esamina solo il blocco di codice sincronizzato che è stato bloccato dall'oggetto t. java non può cercare in tutto il codice per notificare un particolare t.wait ().


0

come da documenti:

Il thread corrente deve possedere il monitor di questo oggetto. Il thread rilascia la proprietà di questo monitor.

wait()metodo significa semplicemente che rilascia il blocco sull'oggetto. Quindi l'oggetto verrà bloccato solo all'interno del blocco / metodo sincronizzato. Se il thread si trova all'esterno del blocco di sincronizzazione significa che non è bloccato, se non è bloccato, cosa rilasceresti sull'oggetto?


0

Thread in attesa sull'oggetto di monitoraggio (oggetto utilizzato dal blocco di sincronizzazione), può esserci n numero di oggetto di monitoraggio nell'intero percorso di un singolo thread. Se Thread attende fuori dal blocco di sincronizzazione, allora non c'è nessun oggetto di monitoraggio e anche altri thread notificano l'accesso per l'oggetto di monitoraggio, quindi come farebbe il thread esterno al blocco di sincronizzazione a sapere che è stato avvisato. Questo è anche uno dei motivi per cui wait (), notify () e notifyAll () si trovano nella classe oggetto piuttosto che nella classe thread.

Fondamentalmente l'oggetto di monitoraggio è una risorsa comune qui per tutti i thread e gli oggetti di monitoraggio possono essere disponibili solo nel blocco di sincronizzazione.

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.