Comunicação entre Threads em Java
Coordene threads Java com wait, notify e notifyAll em monitores compartilhados — e quando preferir primitivas de nível mais alto.
A exclusão mútua garante um estado compartilhado seguro. Ela não permite que uma thread sinalize a outra que o estado mudou. É para isso que serve o trio wait, notify e notifyAll em java.lang.Object. São as primitivas de coordenação de mais baixo nível que o Java expõe — todo mecanismo de nível mais alto (filas bloqueantes, latches, semáforos, Condition) é construído sobre esta ideia: uma thread aguarda dentro de um monitor até que outra thread a instrua a acordar.
O código moderno raramente chama wait/notify diretamente. Você usará BlockingQueue, CountDownLatch ou Condition em vez disso. Mas é preciso conhecer o mecanismo subjacente, porque (a) ainda é o que essas classes usam internamente, (b) toda biblioteca que você lê usa isso, e (c) quando algo dá errado com código de alto nível, o diagnóstico frequentemente chega até um notify que ficou faltando.
O trio
Definido em java.lang.Object, portanto todo objeto os possui:
void wait() throws InterruptedException;
void wait(long timeoutMillis) throws InterruptedException;
void notify();
void notifyAll();A regra fundamental: você só pode chamar esses métodos enquanto detém o monitor do objeto em que os está chamando. Ou seja, você deve estar dentro de um bloco synchronized (obj) { ... } (ou de um método synchronized que bloqueia no mesmo obj). Chamar obj.wait() sem deter o monitor de obj lança IllegalMonitorStateException imediatamente.
synchronized (lock) {
lock.wait(); // ok — we hold lock
lock.notify(); // ok — same
}
lock.wait(); // IllegalMonitorStateExceptionEssa regra é o que faz a API funcionar: o wait e o notify têm a garantia de ocorrer com o lock detido, portanto o estado sobre o qual estão falando é consistente.
O que wait() realmente faz
wait() não é "sleep". Ele realiza atomicamente três coisas:
- Libera o monitor do objeto em que foi chamado.
- Suspende a thread atual no conjunto de espera desse monitor.
- Quando acordada (por
notify/notifyAll/interrupt/timeout) ela reacquire o monitor antes de retornar.
A parte de "liberar e suspender atomicamente" é o que torna wait seguro: um notify que chegasse entre "decidimos esperar" e "de fato começamos a esperar" seria perdido de outra forma. Com wait, essa lacuna não existe.
Depois que wait() retorna, você está de volta dentro do bloco synchronized com o lock detido — é por isso que o código após wait() pode ler o estado compartilhado com segurança.
O que notify() e notifyAll() fazem
notify() seleciona uma thread (a JVM define qual — geralmente não é FIFO) do conjunto de espera e a move de WAITING/TIMED_WAITING para BLOCKED. A thread notificada ainda está aguardando o monitor; o notificador ainda está detendo o monitor. A thread notificada só pode readquirir quando o notificador sai do bloco synchronized.
notifyAll() acorda todas as threads no conjunto de espera da mesma forma. Todas ficam BLOCKED; todas se alinham para o lock; readquirem uma de cada vez conforme o lock fica disponível.
notify é mais rápido (uma thread acordada) mas perigoso: se você acordar a thread errada (aquela cuja condição não está realmente satisfeita), ela volta para wait() e nada útil acontece. notifyAll é mais seguro (algum aguardante que pode progredir o fará) mas mais custoso. Use notifyAll por padrão; troque por notify somente quando puder provar que todos os aguardantes são intercambiáveis.
O padrão obrigatório com loop while
A regra mais importante sobre wait:
Sempre chame
wait()dentro de um loopwhileque reverifica a condição.
synchronized (lock) {
while (!conditionHolds()) {
lock.wait();
}
// now condition holds AND we own the lock
}Três motivos para o loop, não um if:
- Wakeups espúrios. A JVM tem permissão para acordar um
waitsem motivo algum. O loop os captura. notifyAllacorda mais de uma. Quando todas disputam o lock, quem vence pode não ter nada útil a fazer — outra já consumiu o recurso. O loop a manda de volta parawait.- Outro estado pode mudar. Entre o
notifye o momento em que você readquire o lock, outro detentor pode ter desfeito aquilo que você estava aguardando. O loop reverifica.
if (!condition) wait() é o bug mais comum em código wait/notify. Funciona nos testes; quebra em produção às 3 da manhã.
O clássico produtor–consumidor
O caso de uso canônico para wait/notify é um buffer limitado:
class Buffer<T> {
private final Object lock = new Object();
private final Object[] data;
private int count, head, tail;
Buffer(int capacity) { data = new Object[capacity]; }
void put(T item) throws InterruptedException {
synchronized (lock) {
while (count == data.length) lock.wait(); // wait for room
data[tail] = item;
tail = (tail + 1) % data.length;
count++;
lock.notifyAll(); // wake any consumer
}
}
@SuppressWarnings("unchecked")
T take() throws InterruptedException {
synchronized (lock) {
while (count == 0) lock.wait(); // wait for an item
T item = (T) data[head];
data[head] = null;
head = (head + 1) % data.length;
count--;
lock.notifyAll(); // wake any producer
return item;
}
}
}Algumas coisas que esse código faz corretamente:
- Mesmo lock para ambos os métodos (
lock). Um monitor protege todo o estado. - Ambos os waits estão dentro de loops
while. notifyAllnos dois lados — porque produtores e consumidores esperam no mesmo monitor e acordar apenas um pode ser o tipo errado.- Lock detido enquanto aguarda (o
waito libera internamente e o readquire antes de retornar).
Em produção você usaria BlockingQueue em vez de escrever isso à mão. Mas o padrão é o que BlockingQueue faz internamente.
Por que notifyAll é o padrão mais seguro
Se você substituísse notifyAll por notify no buffer acima, haveria um bug sutil. Dois consumidores e um produtor esperam no mesmo monitor. O produtor chama notify; a JVM escolhe uma thread; se escolher um consumidor quando o acordar era para "a fila tem espaço" (irrelevante para consumidores), o consumidor reverifica sua condição (a fila ainda pode estar vazia), volta para wait, e o produtor que deveria acordar nunca é acordado. Fila travada, sem exceção.
Para usar notify com segurança você precisa: todos os aguardantes esperam pela mesma condição, todos são intercambiáveis, e o protocolo garante progresso. Esse é um critério estrito. Use notifyAll por padrão; use notify quando o ganho de desempenho importa e você pode provar o invariante.
As alternativas obsoletas
Há código antigo que usa Thread.suspend() e Thread.resume(). Não use. Eles foram descontinuados no Java 1.2 porque deixam locks detidos e quebram invariantes. O mecanismo wait/notify é a única forma segura de fazer uma thread aguardar outra usando apenas métodos de Object.
Há também Thread.sleep — mas sleep não libera locks. Uma thread que dorme dentro de um bloco synchronized bloqueia todas as outras threads que precisam do mesmo lock até acordar. Use wait (que libera) para qualquer cenário de "aguardar que algo aconteça"; reserve sleep para "aguardar um tempo fixo, sem segurar nada importante."
O que usar em produção
wait/notify são corretos, mas sujeitos a erros. O código moderno prefere os blocos de construção de nível mais alto:
| Necessidade | Use |
|---|---|
| Produtor–consumidor limitado | ArrayBlockingQueue, LinkedBlockingQueue |
| Aguardar N coisas terminarem | CountDownLatch |
| Aguardar N participantes se encontrarem | CyclicBarrier, Phaser |
| Múltiplas variáveis de condição em um lock | Condition (de ReentrantLock.newCondition()) |
| Permissões de recurso | Semaphore |
| Resultado futuro de uso único | CompletableFuture |
Cada um deles tem o loop while correto, a semântica correta de notifyAll/signalAll e o tratamento correto de interrupção incorporados. Vamos conhecê-los todos nesta parte do livro.
Um exemplo detalhado: produtor–consumidor com wait e notifyAll
O programa abaixo executa dois produtores e três consumidores contra o buffer limitado acima. Os produtores colocam 1000 itens cada; os consumidores executam até terem coletivamente retirado 2000.
O que observar na execução:
- As somas coincidiram. Cada item colocado por um produtor foi retirado por exatamente um consumidor; nada foi duplicado, nada foi perdido. Essa é a propriedade de correção do produtor–consumidor, alcançada com apenas um monitor e o par
wait/notifyAll. - O buffer tinha apenas 4 slots, então os produtores o enchiam consistentemente e os consumidores o esvaziavam consistentemente. Os loops
whilepermitiram que eles ficassem em espera e voltassem à espera conforme a fila circulava. Semwait, os produtores teriam girado emcount == capacity, consumindo CPU; com ele, dormem até o consumidor sinalizar. - O
notifyAllfoi chamado no mesmo lock que produtores e consumidores detinham. Esse é todo o mecanismo de coordenação: um monitor, exclusão mútua e sinalização, com o loopwhilecapturando qualquer wakeup que não fosse relevante. - O
waitfinal fora desynchronizedlançouIllegalMonitorStateExceptionimediatamente. Essa é a aplicação da regra pela JVM: você só pode wait/notify em um monitor que detém atualmente. Se você vir essa exceção, o caminho do código chegou awaitsem passar antes porsynchronized. - O mesmo formato — buffer limitado, exclusão mútua, sinal em cada mudança de estado — é o que
ArrayBlockingQueuefaz internamente, exceto que usa doisConditions (um para "não cheio," outro para "não vazio") em vez de um grandenotifyAll. Essa é a forma correta de escrever isso em produção; a versão comwait/notifyAllé o mecanismo subjacente sobre o qual toda classe de nível mais alto é construída.
O que vem a seguir
O próximo capítulo, Java Deadlock, examina o modo de falha que torna o bloqueio sutil em primeiro lugar — duas threads cada uma detendo o que a outra quer — e as estratégias que o previnem.