I metodi wait, notify e notifyAll – Lezione 42 di Java Avanzato

Le condition variable: attesa attiva e passiva

Le condition variable (variabili di condizione) sono un classico meccanismo di sincronizzazione tra thread che consente ai thread di attendere il verificarsi di una determinata condizione che altri thread renderanno vera.
Ad esempio, supponiamo che dei thread condividono una variabile booleana flag. Un thread aspetta che la variabile boolean venga modificata in un ciclo infinito:

  while(flag); // Ciclo vuoto

Il thread non uscirà mai dal ciclo while finchè un altro thread non imposterà flag a false. Questo tipo di attesa che effettua il thread prende il nome di attesa attiva (busy waiting), perchè durante l’attesa il thread spreca inutilmente cicli della CPU. Il thread controlla costantemente la condizione di uscita del ciclo facendosi assegnare dal processore il diritto di eseguire il proprio thread.
Una soluzione più efficiente consiste nel realizzare un’attesa passiva, utilizzando una condition variable ed un mutex. In Java anche le condition variable, così come per i mutex, sono realizzate in modo implicito (non bisogna utilizzare librerie per utilizzarle).

I metodi wait, notify e notifyAll

I principali metodi per realizzare le condition variable sono presenti nella classe Object:

  public void wait() throws InterruptedException;
  public void wait(long timeout) throws InterruptedException;
  public void notify();
  public void notifyAll();

Questi metodi offrono maggiori funzionalità alla sincronizzazione tra thread con l’uso della parola chiave synchronized.

Il metodo wait, chiamato su un oggetto x, mette il thread corrente in attesa che qualche altro thread chiami i metodi notify o notifyAll sullo stesso oggetto x. Wait è un metodo bloccante, e come tutti i metodi bloccanti solleva l’eccezione InterruptedException se interrotto. Quando viene chiamato il metodo wait su un thread, la JVM sospende il metodo e lo aggiunge alla coda d’attesa di thread. Quando viene chiamato il metodo wait, il thread deve possedere il mutex di this altrimenti verrà sollevata un’eccezione.
Esiste una variante di wait che accetta come argomento un intero n che rappresenta dei millisecondi, questa versione consente di far uscire il thread corrente dopo n millisecondi.

Possiamo schematizzare il comportamento di wait su un mutex x in questo modo:

  1. Se il thread corrente non possiede il mutex di x, lancia un’eccezione;
  2. Se lo stato di interruzione del thread corrente è vero, lancia un’eccezione;
  3. In un’unica operazione atomica:
    • Mette il thread corrente nella lista di attesa di x;
    • Rilascia il mutex di x;
    • Sospende l’esecuzione del thread.

Il metodo notify risveglia un thread a caso, il contratto non specifica quale. Mentre il metodo notifyAll risveglia tutti i thread in attesa del mutex, questi thread competeranno tra loro per il mutex quando il thread aspettato (chiamato con wait) esce dalla fase critica.

Quando il thread viene risvegliato con notify:

  1. Se il thread corrente non possiede il mutex di x, lancia un’eccezione;
  2. Preleva un thread dalla coda di attesa di x e lo rende nuovamente eseguibile;
  3. Restituisce il controllo al chiamante.

Quando il thread viene risvegliato con notifyAll:

  1. Se il thread corrente non possiede il mutex di x, lancia un’eccezione;
  2. Preleva tutti i thread dalla coda di attesa di x e li rende nuovamente eseguibili. Questi thread concorreranno per l’acquisizione del mutex di x.;
  3. I thread che non sono riusciti ad acquisire il mutex di x verranno inseriti nuovamente nella lista d’attesa;
  4. Restituisce il controllo al chiamante.

La differenza sostanziale tra notify e notifyAll è che il primo risveglia un singolo thread a caso (il contratto non specifica quale), mentre notifyAll risveglia tutti i thread in attesa. Entrambi i metodi possono essere chiamati su un oggetto x solo se il thread corrente detiene il mutex di x; in caso contrario lanceranno un’eccezione a runtime.

Per race condition si intende una condizione che si verifica quando due processi accedono contemporaneamente ad una risorsa condivisa. Lo stato della risorsa non potrà essere determinato a priori, inoltre potrebbe essere lasciata in uno stato non valido.

Vediamo un esempio tratto dall’esame del 18 giugno 2012 del professor M. Faella:
Implementare il metodo statico threadRace, che accetta due oggetti Runnable come argomenti, li esegue contemporaneamente e restituisce 1 oppure 2, a seconda di quale dei due Runnable è terminato prima. Si noti che threadRace è un metodo bloccante. Sarà valutato negativamente l’uso di attesa attiva.

  class threadRaceTest {
    /**
     * @return
     * -1 : almeno uno dei due thread e' stato interrotto
     * 1 : thread 1 finito per primo
     * 2 : thread 2 finito per primo
    */
    public static synchronized int threadRace (Runnable r1, Runnable r2) {
      Thread t1 = new Thread(r1);
      Thread t2 = new Thread(r2);
      int value = -1;

      try {
        t1.start();
        t2.start();
        t1.join();

        if( t2.isInterrupted() ) return value;

        if( t2.isAlive() ) value = 1;
        else value = 2;

      } catch(InterruptedException e) {
           e.printStackTrace();
      } finally{
           return value;
      }
    }

    public static void main(String[] args) {
      Runnable r = new Runnable() {
        public void run() { }
      };

      System.out.println(threadRace(r, r));
    }
  }

E’ stata implementata con una classe locale l’interfaccia Runnable. Il metodo threadRace fa partire due thread con l’interfaccia appena creata e mette in attesa il primo thread finchè il secondo thread non viene interrotto o terminato. Quando il primo thread riprenderà il controllo effettua i dovuti controlli per determinare se è terminato prima il primo thread o il secondo thread. Il metodo ritorna -1 se uno dei due thread viene interrotto (non previsto dalla traccia).

Nell’attesa di un wait deve essere utilizzato un while e non un if perchè se ci fossero tanti thread in attesa, solo un thread riuscirà ad acquisire il mutex. I restanti thread dovranno richiamare wait per essere inseriti nuovamente nella coda d’attesa. Tutto ciò con un if non sarebbe possibile, funzionerebbe solo alla prima chiamata del wait e successivamente il thread continuerebbe con le proprie operazioni.

Inoltre, il contratto di wait consente i cosiddetti risvegli spuri (Spurious Wakeup) che consentono di uscire da una wait senza un’interruzione o un notify. Questi risvegli spuri avvengono a causa di alcune implementazioni delle system call di certi sistemi operativi. Per ovviare a questo problema basta utilizzare un while e non un if per l’attesa di una wait.

L’interfaccia Condition e Lock

In Java 5 sono state introdotte le interfacce Condition e Lock che consentono di estendere il meccanismo wait/notify e di esplicitare il lock. In particolare abbiamo che:

  public interface Condition {
    void await() throws InterruptedException;
    void signal();
    void signalAll();
  }

Mentre per l’interfaccia Lock:

  public interface Lock{
    void lock();
    void unlock();
    Condition newCondition();
  }

I metodi await, signal e signalAll di Condition sono equivalenti ai metodi wait, notify e notifyAll di Object. Inoltre, ad ogni condition variable viene associato un lock che:

  • Al momento della sospensione del thread mediante await viene liberato;
  • Al risveglio di un thread viene occupato;
  • La creazione di una condition variable avviene con il metodo newCondition del lock associato ad esso.

Ad esempio:

  Lock lockVar = new Mylock();
  // Mylock è una classe che implementa l’interfaccia Lock
  Condition C = lockVar.newCondition();

Indice Lezione PrecedenteLezione Successiva

Pubblicato in Guide, Java, Programmazione Taggato con: , , , , , ,

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

*