Java Threads: Utilizando wait, notify e notifyAll

Veja neste artigo como utilizar os métodos wait, notify e notifyAll para realizar comunicação entre diferentes Threads em Java e garantir o sincronismo e integridade da sua aplicação.

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

Ebook exclusivo
Dê um upgrade no início da sua jornada. Crie sua conta grátis e baixe o e-book

Artigos relacionados