Um assunto muito comum em Java é o uso de Threads, e ao mesmo tempo que este assunto é comum, é também muito confuso. Isso porque abrange diversos conceitos, muitos deles voltados a Sistemas Operacionais.

Queremos neste artigo iniciar explicando o funcionamento de Threads e depois mostraremos como funcionam os eventos de comunicação entre elas, são eles: notify, notifyAll e wait.

Vamos ver um exemplo bem simples na Listagem 1 que irá inicializar duas thread em paralelo e com um contador para cada uma, assim poderemos ver sua ordem de execução pelo processador.


import java.lang.reflect.InvocationTargetException;
public class MyThreadApp {

 /**
  * @param args
  * @throws InvocationTargetException
  * @throws InterruptedException
  */
 public static void main(String[] args) throws InterruptedException, 
 InvocationTargetException {
       Thread t1 = new Thread() {
              @Override
              public void run() {
                     for(int i = 0; i < 10000; i++)
                     System.out.println(i+": t1");
              }

       };

       Thread t2 = new Thread() {
              @Override
              public void run() {
                     for(int i = 0; i < 10000; i++)
                            System.out.println(i+": t2");
              }

       };

       t1.start();
       t2.start();
       
 
 }

}
Listagem 1. Inicializando duas threads

A saída acima será algo parecido com:


0: t1
0: t2
1: t1
1: t2
2: t1
2: t2
3: t1
3: t2
4: t1
4: t2
5: t1
5: t2
6: t2
7: t2
8: t2 …

Quando dizemos “parecido” é porque a saída vai depender de como o escalonador do processador está trabalhando naquele determinado momento, ou seja, não temos como saber ao certo qual será a ordem de execução dessas duas Threads.

Dado o código acima, após a execução da linha t2.start();, quantas Threads nós temos em execução neste exato momento? A resposta mais óbvia seria duas threads, mas na verdade são três. Vamos exemplificar o porque disto.

Há um método na classe Thread chamado activeCount(), que retorna o número de Threads ativas no momento. Então vamos utilizá-lo para comprovar o que falamos anteriormente, temos três threads ativas e não duas. Vamos começar contando as threads antes de executar a linha t1.start(), ou seja, antes de iniciar uma nova thread. Observe o código da Listagem 2.


import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

 /**
  * @param args
  * @throws InvocationTargetException
  * @throws InterruptedException
  */
 public static void main(String[] args) throws InterruptedException, 
 InvocationTargetException {
       Thread t1 = new Thread() {
              @Override
              public void run() {
                     for(int i = 0; i < 10000; i++)
                     System.out.println(i+": t1");
              }

       };

       Thread t2 = new Thread() {
              @Override
              public void run() {
                     for(int i = 0; i < 10000; i++)
                            System.out.println(i+": t2");
              }

       };

       System.out.println("THREADS ATIVAS = "+Thread.activeCount());
       t1.start();
       t2.start();
 
 }

}
Listagem 2. Contando as threads antes de iniciar t1 e t2

A saída do código acima será: THREADS ATIVAS = 1. A única Thread ativa no momento será a Thread main, ou seja, a responsável pela execução do nosso programa principal.

Fique atento a esses conceitos: Thread é um “pedaço” de um processo, ou seja, quando executamos nosso programa acima criamos um processo no Sistema Operacional, e esse nosso processo é composto por 1 Thread (que é a nossa Thread main), mas poderíamos criar quantas Threads forem necessárias em 1 único processo, então imagine um Processo como sendo um pacote de biscoito, e cada biscoito dentro do pacote é uma Thread.

Agora vamos ver quantas Threads teremos após as linhas t1.start() e t2.start(). Observe o código da Listagem 3.


import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

 /**
  * @param args
  * @throws InvocationTargetException
  * @throws InterruptedException
  */
 public static void main(String[] args) throws InterruptedException, 
 InvocationTargetException {
     Thread t1 = new Thread() {
            @Override
            public void run() {
                   for(int i = 0; i < 10000; i++)
                   System.out.println(i+": t1");
            }

     };

     Thread t2 = new Thread() {
            @Override
            public void run() {
                   for(int i = 0; i < 10000; i++)
                          System.out.println(i+": t2");
            }

     };

     
     t1.start();
     t2.start();
     System.out.println("THREADS ATIVAS = "+Thread.activeCount());

 }

}
Listagem 3. Quantidade de Threads após inicialização das threads t1 e t2

O resultado será: 3, pois teremos agora as threads: main, t1 e t2. Vamos agora ver alguns outros exemplos com um pouco mais de complexidade. Observe o código da Listagem 4.


import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

  /**
  * @param args
  * @throws InvocationTargetException
  * @throws InterruptedException
  */
  public static void main(String[] args) throws InterruptedException,
            InvocationTargetException {
     Thread t1 = new Thread() {
            @Override
            public void run() {
                   // for(int i = 0; i < 10000; i++)
                   // System.out.println(i+": t1");
            }

     };

     Thread t2 = new Thread() {
            @Override
            public void run() {
                   // for(int i = 0; i < 10000; i++)
                   // System.out.println(i+": t2");
            }

     };

     

     t1.start();
     t2.start();
     System.out.println("NOME THREAD T1: " + t1.getName() + " | isAlive: "
                   + t1.isAlive());
     System.out.println("NOME THREAD T2: " + t2.getName() + " | isAlive: "
                   + t2.isAlive());

   }

}
Listagem 4. Testando se thread está “viva”

Um problema muito comum em Threads é o controle se uma Thread está “morta” ou “viva”, ou seja, se ainda está em execução ou não. Na Listagem 4 fizemos comentários dentro dos métodos “run()” da Thread t1 e t2 e vamos explicar o porque.

Quando você executar a linha t1.start() será criada uma nova Thread em memória (juntamente com a Thread main que já existia, pois ela fica viva enquanto o programa estiver rodando). Então imagine, a Thread t1 foi criada em memória e está sendo executada, mas a Thread main continua sua execução e cria Thread t2, através do t2.start(), agora a Thread main já criou duas Threads em memória (t1 e t2) e continua sua execução.

Quando a Thread main tentar executar o primeiro System.out.println o resultado será isAlive: false tanto para a Thread t1 como a Thread t2. Mas porque? Isso ocorre, pois quando a Thread main executou o t1.start() a sua execução foi tão rápida (pois não tem nada no método run()) que a Thread t1 acabou morrendo quase que instantaneamente, e o mesmo ocorreu com a Thread t2.

Faça um teste: retire os comentários da Listagem 4 e veja qual será a saída. Provavelmente será “ativa” para as thread t1 e t2, a não ser que seu processador seja tão rápido que antes de executar o primeiro System.out.println ele já terminou a execução da Thread t1 e t2, mas isso também não é uma questão de rapidez e sim de prioridade do escalonador.

Veja no código da Listagem 5 como podemos fazer para mostrar todas as Threads ativas (principalmente a Thread main que tanto falamos).


import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

 /**
  * @param args
  * @throws InvocationTargetException
  * @throws InterruptedException
  */
 public static void main(String[] args) throws InterruptedException,
      InvocationTargetException {
    Thread t1 = new Thread() {
      @Override
      public void run() {
              for(int i = 0; i < 10000; i++)
              System.out.println(i+": t1");
      }

    };

    Thread t2 = new Thread() {
      @Override
      public void run() {
              for(int i = 0; i < 10000; i++)
              System.out.println(i+": t2");
      }

    };



    t1.start();
    t2.start();
    System.out.println("NOME THREAD MAIN: " + Thread.currentThread().getName() + " | isAlive: "
             + Thread.currentThread().isAlive());
    System.out.println("NOME THREAD T1: " + t1.getName() + " | isAlive: "
             + t1.isAlive());
    System.out.println("NOME THREAD T2: " + t2.getName() + " | isAlive: "
             + t2.isAlive());

 }

}
Listagem 5. Mostrando threads ativas e seus nomes

O resultado do código acima será algo parecido com:


0: t1
1: t1
2: t1
3: t1
0: t2
1: t2
NOME THREAD MAIN: main | isAlive: true
NOME THREAD T1: Thread-0 | isAlive: true
NOME THREAD T2: Thread-1 | isAlive: true

Lembre-se que estamos executando os System.out.println na Thread main, então ele pode aparecer no inicio, meio ou fim da sua saída. Perceba que o nome das threads seguem um padrão, começando de zero (que nosso caso é a thread t1) e assim por diante.

Imagine agora que você precisa garantir que a Thread t1 seja executada por completo antes que a Thread t2 inicie, por diversas questão, como por exemplo: Garantir a consistência dos dados que serão lidos na t2 que por algum motivo necessitam de um processamento prévio da t1. Garantimos isso através do método join() da Classe Thread, onde iremos dizer a Thread main que só irá passar para a próxima linha, após toda execução da Thread t1 terminar. Veja nosso código da Listagem 6.


import java.lang.reflect.InvocationTargetException;

public class MyThreadApp {

/**
* @param args
* @throws InvocationTargetException
* @throws InterruptedException
*/
  public static void main(String[] args) throws 
  InterruptedException,
            InvocationTargetException {
     Thread t1 = new Thread() {
            @Override
            public void run() {
                    for(int i = 0; i < 10000; i++)
                    System.out.println(i+": t1");
            }

     };

     Thread t2 = new Thread() {
            @Override
            public void run() {
                    for(int i = 0; i < 10000; i++)
                    System.out.println(i+": t2");
            }

     };

     

     t1.start();
     t1.join();
     t2.start();
     System.out.println("NOME THREAD MAIN: " + Thread.currentThread().getName() 
     + " | isAlive: "
                   + Thread.currentThread().isAlive());
     System.out.println("NOME THREAD T1: " + t1.getName() 
     + " | isAlive: "
                   + t1.isAlive());
     System.out.println("NOME THREAD T2: " + t2.getName() 
     + " | isAlive: "
                   + t2.isAlive());

  }

}
Listagem 6. Usando o join

Olha que interessante, quando a Thread main executar o t1.start() ele vai começar a mostrar os valores de t1 (0,12,3,4,5...), então a Thread main executa o t1.join() e para por ali, ela é congelada até que o método run() da Thread t1 finalize. Finalizado o processamento de t1, a Thread main continua sua execução agora fazendo t2.start() e logo em seguida executa os System.out.println.

No primeiro System.out.println teremos a saída: NOME THREAD MAIN: main | isAlive: true pois enquanto nosso programa estiver rodando a Thread main estará “viva”.

No segundo System.out.println teremos a saída: NOME THREAD T1: Thread-0 | isAlive: false, ou seja, nossa Thread t1 está “morta”, pois como usamos o join, a Thread main foi obrigada a esperar o término de todo processamento de t1 e qualquer código executado depois do t1.join() a Thread t1 já está “morta”.

Por fim, no último System.out.println a Thread t2 continua viva, pois ela está sendo executada em paralelo com a nossa Thread main.

Thread wait, notify e notifyAll

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”.

Vamos partir para 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 chamado b, ou seja, enquanto a Thread A estiver com a “trava” de b 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 objeto b (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 objeto b ser 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 objeto b, pois ele estava como synchronized.
  4. Após terminar todo processamento com o objeto b, a Thread B chamada o b.notify(), para “acordar” a Thread A. Mas atente a 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 do bloco syncronized(),como a Thread A já está acordada, automaticamente ela obtêm a trava do objeto b, 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 acima, 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 um exemplo implementado do cenário acima na Listagem 7.


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 7. Implementação do wait e notify

Saída do Código:


Aguardando o b completar...
Total é igual a: 19900

E se executássemos o mesmo código sem levar em consideração sincronismo? Sem utilizar o synchronized, wait ou notify, assim como na Listagem 2? Veja como fica no código da Listagem 8.


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 8. Implementação sem sincronismo

A saída do código acima é incerto, 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. Neste caso acima 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 como é o nosso caso.

Join versus Wait

Muitos profissionais podem chegar a seguinte conclusão: Quando utilizar Join ou Wait? Ambos não tem a mesma funcionalidade? Baseado na Listagem 3 vamos explicar a diferença entre eles. Observe o código da Listagem 9.


//Usando Join
synchronized(two){  
two.join()  
}
 
//Usando Wait
synchronized(two){  
two.wait();  
}  
   
....  
   
synchronized(two){  
  notify();  
  //or notifyAll();  
}
Listagem 9. 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

Postaremos agora 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, não se preocupe, você não verá códigos em C ou C++, apenas estamos exemplificando tal cenário. Observe a Listagem 10.


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 10. 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, presente na Listagem 11.


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 11. 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.

Com este artigo esperamos que você tenha sido capacitado suficientemente para utilizar tais recursos com tranquilidade e eficácia.