W3docs

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();                                    // IllegalMonitorStateException

Essa 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:

  1. Libera o monitor do objeto em que foi chamado.
  2. Suspende a thread atual no conjunto de espera desse monitor.
  3. 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 loop while que 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:

  1. Wakeups espúrios. A JVM tem permissão para acordar um wait sem motivo algum. O loop os captura.
  2. notifyAll acorda 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 para wait.
  3. Outro estado pode mudar. Entre o notify e 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.
  • notifyAll nos dois lados — porque produtores e consumidores esperam no mesmo monitor e acordar apenas um pode ser o tipo errado.
  • Lock detido enquanto aguarda (o wait o 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:

NecessidadeUse
Produtor–consumidor limitadoArrayBlockingQueue, LinkedBlockingQueue
Aguardar N coisas terminaremCountDownLatch
Aguardar N participantes se encontraremCyclicBarrier, Phaser
Múltiplas variáveis de condição em um lockCondition (de ReentrantLock.newCondition())
Permissões de recursoSemaphore
Resultado futuro de uso únicoCompletableFuture

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.

java— editable, runs on the server

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 while permitiram que eles ficassem em espera e voltassem à espera conforme a fila circulava. Sem wait, os produtores teriam girado em count == capacity, consumindo CPU; com ele, dormem até o consumidor sinalizar.
  • O notifyAll foi 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 loop while capturando qualquer wakeup que não fosse relevante.
  • O wait final fora de synchronized lançou IllegalMonitorStateException imediatamente. 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 a wait sem passar antes por synchronized.
  • O mesmo formato — buffer limitado, exclusão mútua, sinal em cada mudança de estado — é o que ArrayBlockingQueue faz internamente, exceto que usa dois Conditions (um para "não cheio," outro para "não vazio") em vez de um grande notifyAll. Essa é a forma correta de escrever isso em produção; a versão com wait/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.

Prática

Prática
Por que `obj.wait()` deve sempre ser chamado dentro de um bloco `synchronized (obj)` (ou de um método `synchronized` que bloqueia em `obj`)?
Por que `obj.wait()` deve sempre ser chamado dentro de um bloco `synchronized (obj)` (ou de um método `synchronized` que bloqueia em `obj`)?
Was this page helpful?