Coordenação entre Threads em Java: Os Métodos wait() e notify()
Enquanto o synchronized
resolve o problema de exclusão mútua (garantindo que apenas uma thread por vez acesse um recurso), ele não oferece um mecanismo para que as threads coordenem suas ações. Por exemplo, como uma thread pode esperar eficientemente que outra thread altere o estado de um objeto compartilhado?
Para resolver esse problema de comunicação e coordenação, a classe Object
em Java fornece um conjunto de métodos fundamentais:
wait()
: Faz com que a thread atual libere o lock do monitor e entre em um estado de espera até que outra thread invoquenotify()
ounotifyAll()
no mesmo objeto.notify()
: "Acorda" uma única thread que está em estado de espera no monitor do objeto. A escolha de qual thread será acordada não é garantida.notifyAll()
: "Acorda" todas as threads que estão em estado de espera no monitor do objeto.
A regra fundamental é que esses métodos só podem ser chamados de dentro de um contexto sincronizado (um bloco ou método synchronized
). A thread deve possuir o lock do monitor do objeto antes de poder chamar wait()
, notify()
ou notifyAll()
sobre ele.
O Padrão Produtor-Consumidor
A melhor maneira de entender esses métodos é através do clássico problema Produtor-Consumidor. As regras são:
- Um Produtor cria itens e os coloca em um estoque compartilhado.
- Um Consumidor retira itens desse estoque.
- O Consumidor deve esperar se o estoque estiver vazio.
- O Produtor deve esperar se o estoque estiver cheio.
Vamos implementar uma solução onde o produtor deve criar 5 itens e o consumidor deve comprar todos os 5, com um estoque que comporta no máximo 3 itens.
// Classe que representa o estoque compartilhado e o monitor de bloqueio
class Store {
private int productCount = 0;
private final int MAX_PRODUCTS = 3;
public synchronized void get() {
// Enquanto não houver produtos, o consumidor espera
while (productCount < 1) {
try {
wait();
} catch (InterruptedException e) {
System.err.println("Thread interrupted");
Thread.currentThread().interrupt(); // Restaura o status de interrupção
}
}
productCount--;
System.out.println("Consumidor comprou 1 item.");
System.out.println("Itens no estoque: " + productCount);
notify(); // Notifica o produtor que há espaço livre
}
public synchronized void put() {
// Enquanto o estoque estiver cheio, o produtor espera
while (productCount >= MAX_PRODUCTS) {
try {
wait();
} catch (InterruptedException e) {
System.err.println("Thread interrupted");
Thread.currentThread().interrupt();
}
}
productCount++;
System.out.println("Produtor adicionou 1 item.");
System.out.println("Itens no estoque: " + productCount);
notify(); // Notifica o consumidor que um novo item chegou
}
}
// Classe Produtor
class Producer implements Runnable {
Store store;
Producer(Store store) { this.store = store; }
public void run() {
for (int i = 1; i <= 5; i++) {
store.put();
}
}
}
// Classe Consumidor
class Consumer implements Runnable {
Store store;
Consumer(Store store) { this.store = store; }
public void run() {
for (int i = 1; i <= 5; i++) {
store.get();
}
}
}
public class Program {
public static void main(String[] args) {
Store store = new Store();
Producer producer = new Producer(store);
Consumer consumer = new Consumer(store);
new Thread(producer).start();
new Thread(consumer).start();
}
}
Análise da Solução
O Papel do
wait()
: No métodoget()
, o Consumidor primeiro verifica a condição (while (productCount < 1)
). Se o estoque estiver vazio, ele chamawait()
. Este método, de forma atômica, faz duas coisas:- Libera o lock do monitor do objeto
store
. Isso é essencial, pois sem isso, a thread do Produtor jamais poderia adquirir o lock para entrar no métodoput()
e adicionar um item. - Coloca a thread do Consumidor em um estado de espera passiva, sem consumir CPU.
- Libera o lock do monitor do objeto
- O Papel do
notify()
: No métodoput()
, após adicionar um item, o Produtor chamanotify()
. Isso sinaliza a uma das threads em espera (neste caso, o Consumidor) que o estado do objeto mudou e que ela pode tentar acordar. A thread acordada não executa imediatamente. Ela entra em um estado "pronto" e deve competir novamente para readquirir o lock do monitor. Somente após conseguir o lock ela poderá sair dowait()
e continuar sua execução. Por que
while
e nãoif
?: A verificação da condição é feita em um laçowhile
. Isso é uma prática obrigatória por duas razões:- Spurious Wakeups: Uma thread pode, em raras ocasiões, acordar de um
wait()
sem ter sido notificada. O laço garante que a condição seja reavaliada antes de prosseguir, evitando erros. - Múltiplos Consumidores/Produtores: Se houvesse múltiplos consumidores esperando e o produtor adicionasse apenas um item,
notifyAll()
acordaria todos. Apenas um conseguiria o item, e os outros, ao reavaliarem a condição nowhile
, voltariam a esperar corretamente.
- Spurious Wakeups: Uma thread pode, em raras ocasiões, acordar de um
O resultado da execução demonstra essa coordenação perfeita entre as threads:
Produtor adicionou 1 item. Itens no estoque: 1 Produtor adicionou 1 item. Itens no estoque: 2 Produtor adicionou 1 item. Itens no estoque: 3 Consumidor comprou 1 item. Itens no estoque: 2 ...
As threads pausam e retomam suas execuções de forma eficiente e coordenada, respeitando as condições do estoque.
Resumo
- Os métodos
wait()
,notify()
enotifyAll()
são usados para a coordenação e comunicação entre threads. - Eles só podem ser chamados de dentro de um bloco ou método
synchronized
, pela thread que detém o lock do monitor do objeto. wait()
libera o lock do monitor e coloca a thread em um estado de espera eficiente.notify()
acorda uma thread que está esperando no mesmo monitor, permitindo que ela compita novamente pelo lock.- É uma prática essencial verificar a condição de espera dentro de um laço
while
para se proteger contra spurious wakeups e gerenciar múltiplos consumidores/produtores.