O recurso de Threads é muito importante quando é necessário manter o sincronismo entre diferentes processos executados concorrentemente, como no algoritmo do produtor/consumidor, onde determinado processo produz determinado dado e outro processo consome esse dado. Usaremos este algoritmo para exemplificar o uso dos métodos wait, notify e notifyAll e depois daremos exemplos práticos de como fazer tal implementação.

Nota: Um link interessante é esse onde podemos ver de forma bem simplificada o uso de Threads em Java

Vamos iniciar pelos métodos mais comumente utilizados, são eles: Object.wait() e Object.notify(), pois o método Object.notifyAll() será entendido facilmente após explicação dos dois primeiros. O método Object.wait() interrompe a thread atual, ou seja, coloca a mesma para “dormir” até que uma outra thread use o método “Objec.notify()” no mesmo objeto para “acordá-la”.

Iniciemos com um exemplo bem didático para tentar entender o processo em questão:

  1. A thread A está executando seu processamento normalmente, até encontrar um “synchronized()”. Neste ponto a thread A sabe que deve garantir a exclusão mútua do objeto que ela está trabalhando. Vamos supor que tenha um objeto chamadob, ou seja, enquanto a thread A estiver com a “trava” deb ninguém poderá acessá-lo ou fazer qualquer outra operação com o mesmo.
  2. Então com a trava do objeto b garantida, a execução continua até que a mesma encontra um“b.wait()”. Neste ponto a thread A libera o objetob (libera a trava) e “dorme” até que uma outra thread, através do mesmo objeto b, a notifique que ela já pode “acordar”.
  3. Então imagine agora que a thread B que estava aguardando o objetobser liberado começa o seu processamento (já que a thread A está “dormindo”). Mas lembre-se: A thread B já estava em execução, mesmo quando a thread A estava sendo executada, mas ela estava aguardando a thread A liberar o objetob, pois ele estava como synchronized.
  4. Após terminar todo processamento com o objetob,a thread B chamada o“b.notify()”,para “acordar” a thread A. Mas atente há um ponto: diferente do wait (que libera a trava do objeto instantaneamente) o método notify não libera a trava do objeto, apenas acorda a thread que estava dormindo. Sendo assim, mesmo depois de acordar a thread A, a thread B continua sua execução até sair do bloco synchronized(). Ao sair, como a thread A já está acordada, automaticamente ela obtêm a trava do objetob, novamente.
  5. A thread A continua a sua execução logo após a execução do“b.notify()”e termina a mesma com sucesso.

Neste cenário, simples e didático, foi possível perceber o funcionamento total de duas threads conversando entre si através de um objeto e seus métodos wait e notify. Veja a implementação do cenário descrito na Listagem 1.


public class ThreadA {

     public static void main(String[] args){
      ThreadB b = new ThreadB();
      b.start();

      synchronized(b){
          try{
              System.out.println("Aguardando o b completar...");
              b.wait();
          }catch(InterruptedException e){
              e.printStackTrace();
          }

          System.out.println("Total é igual a: " + b.total);
      }
  }
}
public class ThreadB extends Thread {

        int total;
         @Override
         public void run(){
             synchronized(this){
                 for(int i=0; i<200 ; i++){
                     total += i;
                 }
                 notify();
             }
         }
}
Listagem 1. Implementação do wait e notify

Aguardando o b completar...
Total é igual a: 19900
Saída do Código

E se executássemos o mesmo código sem levar em consideração o sincronismo? Ficará como na Listagem 2.


public class ThreadA {
 
       public static void main(String[] args){
        ThreadB b = new ThreadB();
        b.start();
 
                  System.out.println("Total é igual a: " + b.total);
       
    }
}
 
public class ThreadB extends Thread {
 
          int total;
           @Override
           public void run(){
            
                   for(int i=0; i<200 ; i++){
                       total += i;
                   }
               
           }
}
Listagem 2. Implementação sem sincronismo

A saída do código acima é incerta, pode ser: 0, 1, 10, 100 e assim por diante. Isso ocorre, pois a thread A está mostrando o valor de b antes do final da execução da thread B.

Mas e como fica o notifyAll nestes casos acima? O notifyAll tem exatamente a mesma funcionalidade do notify, apenas com um ponto de diferença: o notifyAll em vez de “acordar” apenas uma thread, ele acorda todas as threads que estão aguardando o notify de determinado objeto. No caso descrito no início do artigo, se optássemos por usar o notifyAll no lugar do notify, a thread B acordaria todas as threads que estariam dependendo do objeto b, não só a thread A.

Join versus Wait

Muitos profissionais podem chegar a seguinte conclusão: quando utilizar Join ou Wait ? Ambos não têm a mesma funcionalidade? Baseado na Listagem 3 vamos explicar a diferença entre eles.


//Usando Join
synchronized(two){  
two.join()  
}
 
//Usando Wait
synchronized(two){  
two.wait();  
}  
   
....  
   
synchronized(two){  
  notify();  
  //or notifyAll();  
}
Listagem 3. Join e Wait

Analisando a Listagem 3 você pode perceber que ambos os códigos esperam que o objeto/thread seja liberado para continuar a execução, então vamos as diferenças entre ambos os métodos.

A começar pelo join, este espera até que a thread seja totalmente finalizada, ou seja, seu processamento termine, diferentemente do wait que já libera a thread após o notify, e não necessariamente a thread que chamou o notify precisa ter terminado.

Baseado no caso acima, o código 1 tem a seguinte concepção: “A thread One só vai continuar seu processamento após o término TOTAL da thread Two, ou seja, após o último “;” (ponto e virgula) do código. Enquanto que o código 2 tem a seguinte concepção: “A thread One continuará seu processamento após a thread Two executar um “notify” no objeto two, ou seja, pode ser antes mesmo do seu término.

Exemplo Prático

Agora temos um exemplo muito bom e didático para entender na prática o funcionamento dos métodos wait e notify. Este exemplo demonstra um caso simples de um controlador e uma impressora, onde o controlador envia um sinal para impressora dizendo se ela deve ou não continuar a impressão. É claro que este exemplo é apenas didático, não implementando nenhuma comunicação de baixo nível com a impressora. Observe a Listagem 4.


import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
 
public class ControladorImpressora extends JFrame {
         private JButton btnPausa = null;
         private JScrollPane scrlTexto = new JScrollPane();
         private JTextArea txtArea = new JTextArea();
         private Impressora impressora;
         
         public ControladorImpressora() {
                   super("Exemplo prático Wait e Notify");
                   
                   setLayout(new BorderLayout());
                   add(getBtnPausa(), BorderLayout.NORTH);
                   txtArea.setEditable(false);
                   scrlTexto.add(txtArea);
                   scrlTexto.setViewportView(txtArea);
                   add(scrlTexto, BorderLayout.CENTER);
                   setSize(640,480);
                   setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                   
                   impressora = new Impressora(txtArea);
         }
 
         private JButton getBtnPausa() {
           if (btnPausa == null) {
            btnPausa = new JButton("Pausa");
            btnPausa.addActionListener(new ActionListener() {
              public void actionPerformed(ActionEvent arg0) {
               if (btnPausa.getText().equals("Pausa"))
               {
                 btnPausa.setText("Continua");
                 impressora.setPausado(true);
                 return;
               }
                                               
               btnPausa.setText("Pausa");
               impressora.setPausado(false);
             }
           });
         }
         return btnPausa;
       }
         
       public static void main(String args[]) {
         new ControladorImpressora().setVisible(true);
       }
}
Listagem 4. Implementando nosso Controlador

Acima você pode notar apenas uma janela simples que chama um único método da classe Impressora - o “setPausado” - todo o resto da lógica e processamento você verá mais adiante na classe Impressora (Listagem 5).


import javax.swing.JTextArea;
 
public class Impressora {
       private JTextArea txtDestino = null;
 
       private long linha = 0;
 
       private boolean pausado = false;
 
       public Impressora(JTextArea txtDestino) {
             if (txtDestino == null)
                    throw new NullPointerException
                    ("Destino não pode ser nulo!");
 
             this.txtDestino = txtDestino;
 
             //Disparamos a thread da impressora.
             Thread t = new Thread(new ImpressoraRun(), 
             "Thread da impressora");
             t.setDaemon(true);
             t.start();
       }
 
       /**
        * Nesse método, verificamos a condição que desejamos. 
        * Se a variável pausada valer true, isso nos indica que a thread 
        * deve dormir. Portanto, damos um wait() nela. 
        * Caso contrário, ela deve continuar.
        */
       private synchronized void verificaPausa() 
        throws InterruptedException {
          // Esse while é necessário pois threads estão sujeitas a spurious
          // wakeups, ou seja, elas podem acordar mesmo que nenhum notify       
          // tenha sido dado.
 
          // Whiles diferentes podem ser usados para descrever condições
          // diferentes. Você também pode ter mais de uma condição no while
          // associada com um e. Por exemplo, no caso de um
         //  produtor/consumidor, poderia ser while 
          // (!pausado && !fila.cheia()).
 
          // Nesse caso só temos uma condição, que é dormir quando pausado.
          while (pausado) {
           wait();
          }
       }
 
       /**
        * Nesse método, permitimos a quem quer que use a impressora 
        * que controle sua thread. Definindo pausado como true, 
        * essa thread irá parar e esperar indefinidamente. 
        * Caso pausado seja definido como false, a impressora
        * volta a imprimir.
        */
       public synchronized void setPausado(boolean pausado) {
        this.pausado = pausado;
        // Caso pausado seja definido como false, acordamos a thread e pedimos
        // para ela verificar sua condição. Nesse caso, sabemos que a thread
        // acordará, mas no caso de uma condição com várias alternativas, nem
        // sempre isso seria verdadeiro.
        if (!this.pausado)
           notifyAll();
       }
       
       private void imprime()
       {
             StringBuilder msg = new StringBuilder("Linha ");
             msg.append(Long.toString(linha++ % Long.MAX_VALUE));
             msg.append("\n");
             txtDestino.append(msg.toString());
       }
 
       /**
        * Este é o runnable com a thread da impressora.
        *
        */
       private class ImpressoraRun implements Runnable {
             public void run() {
                    try {
                           while (true) {                                
                                  verificaPausa();
                                  imprime();       
                                  Thread.sleep(500);
                           }
                    } catch (InterruptedException e) {
                           txtDestino.append("Processamento 
                            da impressora interrompido.");
                    }
             }
       }
}
Listagem 5. Implementando a classe Impressora

A classe Impressora está completamente comentada e funcionando, através dos comentários você pode seguir todo o fluxo de processamento da mesma. É um ótimo exemplo para estudo, além de prover facilidade no entendimento e prática do mesmo.

Portanto, este artigo teve como principal objeto mostrar a teoria e prática aplicadas ao conceito de threads para utilização dos métodos wait, notify e notifyAll. Além disso, mostramos também a diferença entre o Thread.join e o Object.wait, ambos conceitos se confundem e acabam trazendo grande dor de cabeça em alguns momentos.

Confira também