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.
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:
- 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.
- 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”.
- 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.
- 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.
- 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();
}
}
}
Aguardando o b completar...
Total é igual a: 19900
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;
}
}
}
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();
}
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);
}
}
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.");
}
}
}
}
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.